daemon

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 11, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package daemon implements the long-running gpgsmith daemon process.

The daemon holds open vaults in memory across many client RPCs, runs the per-session heartbeat goroutines that keep encrypted ephemeral state files fresh on disk, and serves the wire ConnectRPC API on a Unix domain socket. CLI / TUI / web-UI frontends are thin clients of this daemon: they speak Connect over the socket and never touch GPG or the vault directly.

Layering:

pkg/gpgsmith            kernel: Session, ephemeral, hardening
pkg/vault, pkg/gpg, pkg/audit
  ^
pkg/wire                hand-written ConnectRPC handlers + Backend interface
  ^
pkg/daemon              THIS PACKAGE — Backend implementation + lifecycle

The daemon implements wire.Backend; every method on the interface is routed to either kernel session-bearing operations (open / mutate / seal) or one of the per-vault GPG / audit / publish helpers, with the session map providing the bridge between a vault name and the in-memory *gpgsmith.Session.

Index

Constants

View Source
const (
	// DefaultIdleTimeout is the default per-session idle window after
	// which the daemon auto-seals to the encrypted ephemeral file and
	// drops the in-memory workdir.
	DefaultIdleTimeout = 5 * time.Minute

	// DefaultGracefulTimeout caps how long DaemonShutdown will spend
	// auto-sealing open sessions before falling back to discarding the
	// rest.
	DefaultGracefulTimeout = 30 * time.Second
)
View Source
const (
	// DefaultRingSize is the per-topic ring buffer length used when a
	// subscriber does not request an explicit one. Old enough to let a
	// late subscriber catch up on a typical job, small enough to bound
	// daemon memory in the face of many topics.
	DefaultRingSize = 64
)

Variables

View Source
var (
	// ErrSessionNotOpen is returned by methods that require an open
	// session when no session exists for the requested token.
	ErrSessionNotOpen = errors.New("daemon: no open session for token")

	// ErrShuttingDown is returned by OpenVault / ResumeVault while the
	// daemon is in the process of shutting down.
	ErrShuttingDown = errors.New("daemon: shutting down")
)

Functions

func BindSocket

func BindSocket(path string) (net.Listener, error)

BindSocket binds a Unix domain socket listener at the given path, recovering from a stale socket file left behind by a crashed daemon.

The standard idiom: try Listen; on EADDRINUSE try Dial; if Dial succeeds another daemon is alive (return AlreadyRunningError); if Dial fails with ECONNREFUSED the file is stale, remove it and retry the bind. The returned listener has its socket file at mode 0600.

func IsAlreadyRunning

func IsAlreadyRunning(err error) bool

IsAlreadyRunning reports whether err is an AlreadyRunningError.

func SocketPath

func SocketPath() (string, error)

SocketPath returns the canonical Unix socket path for the gpgsmith daemon. On Linux it lives under XDG_RUNTIME_DIR (which is per-user and cleaned up on logout); on macOS it lives under TMPDIR. The parent directory is created with mode 0700 if it does not exist.

Types

type AlreadyRunningError

type AlreadyRunningError struct {
	Path string
}

AlreadyRunningError is returned by BindSocket when another daemon process is already listening on the requested socket path.

func (*AlreadyRunningError) Error

func (e *AlreadyRunningError) Error() string

Error implements the error interface.

type Broker

type Broker struct {
	// contains filtered or unexported fields
}

Broker is the daemon's in-process pub/sub fan-out for wire.Event. Topics are arbitrary strings; the daemon uses "vault:<name>" and "job:<id>" topic naming, but the broker itself is generic.

Each topic keeps a small ring buffer of recent events so that a subscriber that joins after a publish still sees the recent history (last DefaultRingSize events by default). Slow consumers must not block publishers: if a subscriber's channel is full, the event is dropped for that subscriber and a debug message is logged.

func NewBroker

func NewBroker(logger *slog.Logger) *Broker

NewBroker constructs an empty Broker. Logger is used for slow-subscriber drop warnings; pass slog.Default() if you don't have one handy.

func (*Broker) CloseAll

func (b *Broker) CloseAll()

CloseAll closes every subscriber channel and clears all topics. Used during daemon shutdown to signal in-flight Subscribe RPCs that the stream is over.

func (*Broker) Publish

func (b *Broker) Publish(topic string, evt *wire.Event)

Publish appends evt to the topic's ring buffer (evicting the oldest if at capacity) and fans it out to every current subscriber on the topic. Subscribers whose channels are full have the event dropped (logged at debug level). Publish never blocks on a slow subscriber.

func (*Broker) Subscribe

func (b *Broker) Subscribe(topic string, bufSize int) (subID uint64, ch <-chan *wire.Event, unsubscribe func())

Subscribe attaches a new subscriber to the given topic. The returned channel receives events as they are published; bufSize controls the channel's buffer (a slow subscriber whose channel is full will have events dropped). The unsubscribe function detaches the subscriber and closes the channel; safe to call multiple times.

Any events currently in the topic's ring buffer are replayed into the new subscriber's channel before any new events are forwarded. The replay respects bufSize (oldest entries are dropped if the buffer can't hold them all), so a subscriber that asks for bufSize=1 just gets the most recent event plus future ones.

type Daemon

type Daemon struct {
	// contains filtered or unexported fields
}

Daemon implements wire.Backend against the gpgsmith kernel. It owns a map of open sessions keyed by opaque session token, an event broker for pub/sub fan-out, and a cancellation channel that the lifecycle goroutine in lifecycle.go uses to coordinate graceful shutdown.

Multiple open sessions for the same vault name are allowed and are fully independent: each has its own token, its own /dev/shm workdir, and its own gpg-agent pair. They do NOT share backing state.

All methods on Daemon are safe to call from multiple goroutines concurrently. Per-session state is protected by the per-entry mutex; the sessions map itself is protected by Daemon.mu.

func New

func New(opts Options) *Daemon

New constructs a Daemon with the given options. The returned Daemon is not yet listening on a socket; call Run to start the lifecycle.

func (*Daemon) AddIdentity

func (d *Daemon) AddIdentity(ctx context.Context, token, uid string) error

AddIdentity implements wire.Backend.

func (*Daemon) AddPublishServer

func (d *Daemon) AddPublishServer(_ context.Context, token, alias, url string) error

AddPublishServer implements wire.Backend.

func (*Daemon) Broker

func (d *Daemon) Broker() *Broker

Broker returns the daemon's event broker. Exposed for tests; production callers should use SubscribeEvents.

func (*Daemon) CardInventory

func (d *Daemon) CardInventory(_ context.Context, token string) ([]gpg.YubiKeyEntry, error)

CardInventory implements wire.Backend.

func (*Daemon) CreateMasterKey

func (d *Daemon) CreateMasterKey(ctx context.Context, token string, opts wire.CreateKeyOpts) (string, []gpg.SubKey, error)

CreateMasterKey implements wire.Backend.

func (*Daemon) CreateVault

func (d *Daemon) CreateVault(ctx context.Context, name, path, passphrase string) (vault.Snapshot, wire.SessionInfo, string, error)

CreateVault implements wire.Backend. Initializes a brand-new vault registry entry, writes an empty snapshot to disk, opens it, and registers a session for follow-up key generation via KeyService.Create.

func (*Daemon) DaemonShutdown

func (d *Daemon) DaemonShutdown(ctx context.Context, gracefulTimeoutSeconds int) error

DaemonShutdown implements wire.Backend. Marks the daemon as shutting down (rejecting new opens), then auto-seals every open session within the supplied graceful budget; sessions that exceed the budget are force-discarded. Finally signals the lifecycle goroutine to close the listener and exit.

func (*Daemon) DaemonStatus

func (d *Daemon) DaemonStatus(_ context.Context) (wire.DaemonStatus, error)

DaemonStatus implements wire.Backend.

func (*Daemon) DisablePublishServer

func (d *Daemon) DisablePublishServer(_ context.Context, token, alias string) error

DisablePublishServer implements wire.Backend.

func (*Daemon) DiscardVault

func (d *Daemon) DiscardVault(ctx context.Context, token string) error

DiscardVault implements wire.Backend.

func (*Daemon) DiscoverCard

func (d *Daemon) DiscoverCard(ctx context.Context, token, label, description string) (gpg.YubiKeyEntry, bool, error)

DiscoverCard implements wire.Backend.

func (*Daemon) EnablePublishServer

func (d *Daemon) EnablePublishServer(_ context.Context, token, alias string) error

EnablePublishServer implements wire.Backend.

func (*Daemon) ExportKey

func (d *Daemon) ExportKey(ctx context.Context, token string) (string, error)

ExportKey implements wire.Backend.

func (*Daemon) ExportVault

func (d *Daemon) ExportVault(ctx context.Context, name, passphrase, targetDir string) (string, error)

ExportVault implements wire.Backend. Decrypts the latest canonical of the named vault and copies its contents into targetDir. This is an offline, one-shot operation: no session is created, no daemon-side state is touched.

func (*Daemon) GenerateSubkeys

func (d *Daemon) GenerateSubkeys(ctx context.Context, token string) ([]gpg.SubKey, error)

GenerateSubkeys implements wire.Backend.

func (*Daemon) ImportVault

func (d *Daemon) ImportVault(ctx context.Context, sourcePath, passphrase, targetName string) (vault.Snapshot, error)

ImportVault implements wire.Backend.

func (*Daemon) KeyStatus

func (d *Daemon) KeyStatus(ctx context.Context, token string) ([]gpg.SubKey, *gpg.CardInfo, error)

KeyStatus implements wire.Backend.

func (*Daemon) ListIdentities

func (d *Daemon) ListIdentities(ctx context.Context, token string) ([]gpg.UID, error)

ListIdentities implements wire.Backend.

func (*Daemon) ListKeys

func (d *Daemon) ListKeys(ctx context.Context, token string) ([]gpg.SubKey, error)

ListKeys implements wire.Backend.

func (*Daemon) ListPublishServers

func (d *Daemon) ListPublishServers(_ context.Context, token string) ([]gpg.ServerEntry, error)

ListPublishServers implements wire.Backend.

func (*Daemon) ListSessionTokens added in v0.5.0

func (d *Daemon) ListSessionTokens(_ context.Context) ([]wire.SessionTokenEntry, error)

ListSessionTokens implements wire.Backend. Returns the opaque session tokens alongside each session's vault name for the local CLI auto-bind path. The tokens are NOT exposed via proto messages; they travel only in a response header stamped by the wire layer.

func (*Daemon) ListSessions

func (d *Daemon) ListSessions(_ context.Context) ([]wire.SessionInfo, error)

ListSessions implements wire.Backend.

func (*Daemon) ListVaults

func (d *Daemon) ListVaults(_ context.Context) ([]vault.Entry, string, error)

ListVaults implements wire.Backend.

Deduplicates against the legacy single-vault form: if cfg.VaultDir is set AND a registry entry already covers the same name (typically "default") OR the same physical path, the legacy entry is suppressed. This avoids the user seeing two rows for the same vault when their config carries both `vault_dir:` and an explicit `vaults:` entry — a state that occurs naturally after TOFU first-use writes the registry entry without removing the legacy field.

func (*Daemon) LookupPublished

func (d *Daemon) LookupPublished(ctx context.Context, token string) ([]wire.LookupResult, error)

LookupPublished implements wire.Backend.

func (*Daemon) OpenVault

func (d *Daemon) OpenVault(ctx context.Context, name, passphrase string, source gpgsmith.LockSource) (wire.OpenResult, string, error)

OpenVault implements wire.Backend. If a recoverable ephemeral exists on disk for our hostname the call returns OpenResult{ResumeAvailable: ...} without opening — the caller must follow up with ResumeVault. Otherwise the latest canonical is opened, TOFU is performed, and the new session is recorded in d.sessions.

func (*Daemon) PrimaryIdentity

func (d *Daemon) PrimaryIdentity(ctx context.Context, token, uid string) error

PrimaryIdentity implements wire.Backend.

func (*Daemon) ProvisionCard

func (d *Daemon) ProvisionCard(ctx context.Context, token string, opts wire.ProvisionCardOpts) (gpg.YubiKeyEntry, string, error)

ProvisionCard implements wire.Backend.

func (*Daemon) Publish

func (d *Daemon) Publish(ctx context.Context, token string, aliases []string) ([]wire.PublishResult, error)

Publish implements wire.Backend.

func (*Daemon) RemovePublishServer

func (d *Daemon) RemovePublishServer(_ context.Context, token, alias string) error

RemovePublishServer implements wire.Backend.

func (*Daemon) ResumeVault

func (d *Daemon) ResumeVault(ctx context.Context, name, passphrase string, source gpgsmith.LockSource, resume bool) (wire.SessionInfo, string, error)

ResumeVault implements wire.Backend.

func (*Daemon) RevokeCard

func (d *Daemon) RevokeCard(ctx context.Context, token, label string) error

RevokeCard implements wire.Backend.

func (*Daemon) RevokeIdentity

func (d *Daemon) RevokeIdentity(ctx context.Context, token, uid string) error

RevokeIdentity implements wire.Backend.

func (*Daemon) RevokeSubkey

func (d *Daemon) RevokeSubkey(ctx context.Context, token, keyID string) error

RevokeSubkey implements wire.Backend.

func (*Daemon) RotateCard

func (d *Daemon) RotateCard(ctx context.Context, token, label string) (gpg.YubiKeyEntry, error)

RotateCard implements wire.Backend.

func (*Daemon) Run

func (d *Daemon) Run(ctx context.Context) error

Run is the daemon main loop: hardens the process, binds the Unix socket, serves the wire ConnectRPC API over h2c (so server-streaming RPCs like EventService.Subscribe work over cleartext HTTP/2), and handles SIGINT / SIGTERM / SIGHUP with a graceful shutdown.

Run blocks until the listener is closed (shutdown) or an error occurs. It returns nil on normal shutdown.

func (*Daemon) SSHPubKey

func (d *Daemon) SSHPubKey(ctx context.Context, token string) (string, error)

SSHPubKey implements wire.Backend.

func (*Daemon) SealVault

func (d *Daemon) SealVault(ctx context.Context, token, message string) (vault.Snapshot, error)

SealVault implements wire.Backend.

func (*Daemon) ShowAudit

func (d *Daemon) ShowAudit(_ context.Context, token string, last int) ([]audit.Entry, error)

ShowAudit implements wire.Backend.

func (*Daemon) ShutdownCh

func (d *Daemon) ShutdownCh() <-chan struct{}

ShutdownCh returns a channel that is closed when DaemonShutdown is invoked. Used by the lifecycle goroutine to drive listener teardown.

func (*Daemon) Snapshots

func (d *Daemon) Snapshots(ctx context.Context, name string) ([]vault.Snapshot, error)

Snapshots implements wire.Backend.

func (*Daemon) SocketPath

func (d *Daemon) SocketPath() string

SocketPath returns the configured Unix socket path or "" if Run has not yet computed one.

func (*Daemon) StatusVaults

func (d *Daemon) StatusVaults(_ context.Context) ([]wire.SessionInfo, []wire.ResumeOption, error)

StatusVaults implements wire.Backend. Returns currently-open sessions from the daemon plus any recoverable ephemerals discovered on disk for vaults that are NOT currently open.

func (*Daemon) SubscribeEvents

func (d *Daemon) SubscribeEvents(ctx context.Context, token string) (<-chan wire.Event, error)

SubscribeEvents implements wire.Backend. Returns a channel that receives events. If token names an open session the channel is filtered to that vault; if token is empty the subscriber sees all events. The channel is closed when ctx is canceled or the daemon is shutting down.

func (*Daemon) TrustVault

func (d *Daemon) TrustVault(_ context.Context, name, fingerprint string) error

TrustVault implements wire.Backend.

type Options

type Options struct {
	Version         string
	Commit          string
	Logger          *slog.Logger
	IdleTimeout     time.Duration
	GracefulTimeout time.Duration
	// ConfigPath overrides the vault registry config file path. If
	// empty, vault.LoadConfig("") is used (which resolves to the
	// default per-user location).
	ConfigPath string
	// SocketPath overrides the Unix socket path. If empty, the
	// daemon will compute it via SocketPath() in Run.
	SocketPath string
}

Options configures a new Daemon.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL