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
- Variables
- func BindSocket(path string) (net.Listener, error)
- func IsAlreadyRunning(err error) bool
- func SocketPath() (string, error)
- type AlreadyRunningError
- type Broker
- type Daemon
- func (d *Daemon) AddIdentity(ctx context.Context, token, uid string) error
- func (d *Daemon) AddPublishServer(_ context.Context, token, alias, url string) error
- func (d *Daemon) Broker() *Broker
- func (d *Daemon) CardInventory(_ context.Context, token string) ([]gpg.YubiKeyEntry, error)
- func (d *Daemon) CreateMasterKey(ctx context.Context, token string, opts wire.CreateKeyOpts) (string, []gpg.SubKey, error)
- func (d *Daemon) CreateVault(ctx context.Context, name, path, passphrase string) (vault.Snapshot, wire.SessionInfo, string, error)
- func (d *Daemon) DaemonShutdown(ctx context.Context, gracefulTimeoutSeconds int) error
- func (d *Daemon) DaemonStatus(_ context.Context) (wire.DaemonStatus, error)
- func (d *Daemon) DisablePublishServer(_ context.Context, token, alias string) error
- func (d *Daemon) DiscardVault(ctx context.Context, token string) error
- func (d *Daemon) DiscoverCard(ctx context.Context, token, label, description string) (gpg.YubiKeyEntry, bool, error)
- func (d *Daemon) EnablePublishServer(_ context.Context, token, alias string) error
- func (d *Daemon) ExportKey(ctx context.Context, token string) (string, error)
- func (d *Daemon) ExportVault(ctx context.Context, name, passphrase, targetDir string) (string, error)
- func (d *Daemon) GenerateSubkeys(ctx context.Context, token string) ([]gpg.SubKey, error)
- func (d *Daemon) ImportVault(ctx context.Context, sourcePath, passphrase, targetName string) (vault.Snapshot, error)
- func (d *Daemon) KeyStatus(ctx context.Context, token string) ([]gpg.SubKey, *gpg.CardInfo, error)
- func (d *Daemon) ListIdentities(ctx context.Context, token string) ([]gpg.UID, error)
- func (d *Daemon) ListKeys(ctx context.Context, token string) ([]gpg.SubKey, error)
- func (d *Daemon) ListPublishServers(_ context.Context, token string) ([]gpg.ServerEntry, error)
- func (d *Daemon) ListSessionTokens(_ context.Context) ([]wire.SessionTokenEntry, error)
- func (d *Daemon) ListSessions(_ context.Context) ([]wire.SessionInfo, error)
- func (d *Daemon) ListVaults(_ context.Context) ([]vault.Entry, string, error)
- func (d *Daemon) LookupPublished(ctx context.Context, token string) ([]wire.LookupResult, error)
- func (d *Daemon) OpenVault(ctx context.Context, name, passphrase string, source gpgsmith.LockSource) (wire.OpenResult, string, error)
- func (d *Daemon) PrimaryIdentity(ctx context.Context, token, uid string) error
- func (d *Daemon) ProvisionCard(ctx context.Context, token string, opts wire.ProvisionCardOpts) (gpg.YubiKeyEntry, string, error)
- func (d *Daemon) Publish(ctx context.Context, token string, aliases []string) ([]wire.PublishResult, error)
- func (d *Daemon) RemovePublishServer(_ context.Context, token, alias string) error
- func (d *Daemon) ResumeVault(ctx context.Context, name, passphrase string, source gpgsmith.LockSource, ...) (wire.SessionInfo, string, error)
- func (d *Daemon) RevokeCard(ctx context.Context, token, label string) error
- func (d *Daemon) RevokeIdentity(ctx context.Context, token, uid string) error
- func (d *Daemon) RevokeSubkey(ctx context.Context, token, keyID string) error
- func (d *Daemon) RotateCard(ctx context.Context, token, label string) (gpg.YubiKeyEntry, error)
- func (d *Daemon) Run(ctx context.Context) error
- func (d *Daemon) SSHPubKey(ctx context.Context, token string) (string, error)
- func (d *Daemon) SealVault(ctx context.Context, token, message string) (vault.Snapshot, error)
- func (d *Daemon) ShowAudit(_ context.Context, token string, last int) ([]audit.Entry, error)
- func (d *Daemon) ShutdownCh() <-chan struct{}
- func (d *Daemon) Snapshots(ctx context.Context, name string) ([]vault.Snapshot, error)
- func (d *Daemon) SocketPath() string
- func (d *Daemon) StatusVaults(_ context.Context) ([]wire.SessionInfo, []wire.ResumeOption, error)
- func (d *Daemon) SubscribeEvents(ctx context.Context, token string) (<-chan wire.Event, error)
- func (d *Daemon) TrustVault(_ context.Context, name, fingerprint string) error
- type Options
Constants ¶
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 )
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 ¶
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 ¶
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 ¶
IsAlreadyRunning reports whether err is an AlreadyRunningError.
func SocketPath ¶
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 ¶
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 ¶
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 ¶
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 ¶
AddIdentity implements wire.Backend.
func (*Daemon) AddPublishServer ¶
AddPublishServer implements wire.Backend.
func (*Daemon) Broker ¶
Broker returns the daemon's event broker. Exposed for tests; production callers should use SubscribeEvents.
func (*Daemon) CardInventory ¶
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 ¶
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 ¶
DaemonStatus implements wire.Backend.
func (*Daemon) DisablePublishServer ¶
DisablePublishServer implements wire.Backend.
func (*Daemon) DiscardVault ¶
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 ¶
EnablePublishServer 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 ¶
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) ListIdentities ¶
ListIdentities implements wire.Backend.
func (*Daemon) ListPublishServers ¶
ListPublishServers implements wire.Backend.
func (*Daemon) ListSessionTokens ¶ added in v0.5.0
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 ¶
ListSessions implements wire.Backend.
func (*Daemon) ListVaults ¶
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 ¶
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 ¶
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 ¶
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 ¶
RevokeCard implements wire.Backend.
func (*Daemon) RevokeIdentity ¶
RevokeIdentity implements wire.Backend.
func (*Daemon) RevokeSubkey ¶
RevokeSubkey implements wire.Backend.
func (*Daemon) RotateCard ¶
RotateCard implements wire.Backend.
func (*Daemon) Run ¶
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) 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) SocketPath ¶
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 ¶
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.
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.