docker / docker/secrets-engine

Linux keychain dials a new D-Bus connection per operation, defeating per-entry access confirmation

Open
#648 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
91
Forks
16
Avg merge
9h 3m
Merged PRs (30d)
24

Description

Problem

The Linux keychain store dials a private D-Bus connection, opens a Secret
Service session, and closes both on every operation (operationService in
store/keychain/keychain_linux.go, kc.NewService in
store/keychain/internal/go-keychain/secretservice/secretservice.go).

Each connection presents a new unique bus name, so the provider sees a new
client on every call. docker/sbx-releases#579 reports the result: with
per-entry access confirmation enabled, one login produced 61 D-Bus
connections, 119 OpenSession calls, and roughly 300 confirmation popups —
more than a user can confirm before sbx's own timeout expires.

Per-call churn causes three failures:

  • "Remember this application" binds to the connection's unique bus name, so
    the user's choice never applies to the next operation.
  • Unlocks die with their connection, so every operation repeats the
    IsLockedUnlockPrompt cycle that withRelockRetry exists to
    absorb.
  • Every operation pays connection setup, session negotiation, and teardown.

What the spec says

The Secret Service spec binds a session to the client's bus connection: it
closes on disconnect or explicit Close(), has no timeout, and calls
multiple sessions per client "typically unnecessary"
(https://specifications.freedesktop.org/secret-service/latest/sessions.html).
libsecret, the reference client, keeps one process-global connection and
session. Reuse is the intended model.

Proposal

Cache one (SecretService, Session) pair per store, dial it lazily, and
reuse it for all operations.

  • Serialize operations with a mutex: PromptAndWait shares one signal
    channel per connection and is not thread-safe. (Alternative: route
    Completed signals by prompt path.)
  • Dial the cached connection under a detached context, so a caller's
    cancellation cannot kill the shared connection; keep per-operation
    contexts for prompt waits.
  • Drop the cache and redial once on stale-state errors —
    org.freedesktop.Secret.Error.NoSession, NoSuchObject,
    ServiceUnknown, or a closed connection — matched on structured D-Bus
    error names, as isLockedDBusError does today.
  • Give the store a teardown path for the cached connection (see Risks).
  • Keep withRelockRetry as a safety net and keep ensureAvailable's
    short-lived probe connection.

Risks

The store model changes: state and teardown. Today every operation is
self-contained — dial, work, close — so a store holds no resources between
calls and needs no teardown. A cached connection makes the store stateful.
store.Store has no Close; the Linux store would need a teardown function
(for example Close() error behind an optional interface), and callers
would own calling it — including embedders that construct stores freely,
one per request or per test.

Open D-Bus connections can leak. An abandoned store holds one live bus
connection — a unix-socket fd, a unique bus name, an open session — until
process exit. The bus daemon caps connections per user, so in a long-lived
process the leak is real, not cosmetic.

Mitigation. An idle timeout bounds both risks without an API change:
close the cached connection after a short idle period, redial on next use.
A run still collapses to one connection, and an abandoned store self-heals.
Timeout and teardown hook compose; the timeout alone may suffice.

Expected effect

One connection per run. Unlocks and "remember this application" persist
across operations. Prompt count drops from one cycle per operation to one
confirmation per entry — or one total, once remembered.

Refs docker/sbx-releases#579.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with operationService in store/keychain/keychain_linux.go and kc.NewService in store/keychain/internal/go-keychain/secretservice/secretservice.go, then inspect store.Store lifecycle and withRelockRetry. The completed change should reuse a per-store SecretService session safely across operations, recover from the listed stale D-Bus errors, and provide bounded cleanup without breaking the short-lived ensureAvailable probe.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, linux
Domain
backend, security
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.