agent

package
v0.30.0 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package agent implements dotvault's SSH agent surface: a read-only agent.ExtendedAgent backend served over a Unix domain socket (Linux/macOS) or a named pipe (Windows). Signing capability is exposed over dotvault's live, renewing Vault token without ever writing private keys to disk.

The backend is platform-neutral and concurrency-safe; both platform listeners (listener_unix.go, listener_windows.go) serve the same instance. Identities come from one or more Source implementations — raw keys read from KV, or short-lived certificates minted by a Vault SSH CA. dotvault is one-way, so the agent is too: Add/Remove/Lock and friends return a read-only error.

Index

Constants

This section is empty.

Variables

View Source
var ErrKeyNotFound = fmt.Errorf("no matching key")

ErrKeyNotFound is returned by Sign when no source owns the requested key.

View Source
var ErrReadOnly = errors.New("dotvault agent is read-only")

ErrReadOnly is returned by every mutating agent operation. dotvault syncs one way (Vault → local); the agent mirrors that and never accepts keys, locks, or removals from clients.

Functions

func ResolveEndpoint

func ResolveEndpoint(agentCfg config.AgentConfig) string

ResolveEndpoint picks the platform endpoint, applying per-user defaults when the config leaves the path/pipe empty. Exported so the CLI status command can report the endpoint without constructing a full Service.

Types

type Backend

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

Backend is the platform-neutral agent.ExtendedAgent served by both listeners. It is safe for concurrent use: List results are cached behind a short TTL and every Sign is serviced independently.

func NewBackend

func NewBackend(sources []Source, opts ...Option) *Backend

NewBackend builds a backend over the given ordered sources.

func (*Backend) Add

func (b *Backend) Add(key agent.AddedKey) error

func (*Backend) Extension

func (b *Backend) Extension(extensionType string, contents []byte) ([]byte, error)

Extension reports no extensions are supported.

func (*Backend) List

func (b *Backend) List() ([]*agent.Key, error)

List enumerates the available identities (cached briefly).

func (*Backend) Lock

func (b *Backend) Lock(passphrase []byte) error

func (*Backend) Remove

func (b *Backend) Remove(key ssh.PublicKey) error

func (*Backend) RemoveAll

func (b *Backend) RemoveAll() error

func (*Backend) SetReauthGate

func (b *Backend) SetReauthGate(g ReauthGate)

SetReauthGate wires the gate after construction. Safe to call concurrently with Sign — the store is atomic — though in practice the daemon sets it once, before the listener begins accepting connections. A nil argument is a no-op (the gate cannot be un-wired); nothing relies on clearing it.

func (*Backend) Sign

func (b *Backend) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error)

Sign signs data with the key, defaulting the signature algorithm.

func (*Backend) SignWithFlags

func (b *Backend) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error)

SignWithFlags matches key to a source and signs data, honouring the rsa-sha2 flags. If the daemon is mid-reauth it waits up to reauthTimeout for a usable token before failing.

A source that errors (e.g. a vault-ca source whose role can't currently mint) is skipped rather than aborting the whole call, mirroring identities(): a source's own failure must not deny signing for a key owned by a different, healthy source. The error only surfaces if no source ends up matching the key, so a genuine "no source can produce this signature" case still reports why.

func (*Backend) Signers

func (b *Backend) Signers() ([]ssh.Signer, error)

func (*Backend) Status

func (b *Backend) Status(ctx context.Context) Status

Status gathers a fresh snapshot, querying every source. A source that fails to resolve (unknown engine, Vault read error, missing CA role) is reported with its Error set rather than aborting the whole snapshot — mirroring the per-rule isolation of the sync engine.

func (*Backend) Unlock

func (b *Backend) Unlock(passphrase []byte) error

type Identity

type Identity struct {
	// PubKey is the key advertised over List and matched on Sign. For
	// certificate sources this is the *ssh.Certificate (its Marshal returns
	// the cert blob, which is what a client requests on Sign).
	PubKey ssh.PublicKey

	// Comment is the human-facing label shown by `ssh-add -l`.
	Comment string

	// Expiry is the certificate validity end for cert identities; the zero
	// value means "no expiry" (a raw key).
	Expiry time.Time
}

Identity is a public key or certificate the agent can present.

type IdentityStatus

type IdentityStatus struct {
	Comment     string `json:"comment,omitempty"`
	Fingerprint string `json:"fingerprint"`
	IsCert      bool   `json:"is_cert"`
	// ExpiresAt / TTLSeconds are populated only for certificates with a
	// bounded validity window.
	ExpiresAt  string `json:"expires_at,omitempty"`
	TTLSeconds int64  `json:"ttl_seconds,omitempty"`
}

IdentityStatus describes a single advertised key or certificate.

func QueryListening

func QueryListening(ctx context.Context, addr string) ([]IdentityStatus, error)

QueryListening connects to a running daemon's agent endpoint and returns the identities it is currently serving, obtained over the SSH agent protocol — the equivalent of `ssh-add -l`. This reports what the live daemon actually offers (the cached minted certificate with its true remaining validity, the keys presently discoverable in Vault) rather than a static description of config, and it never creates the endpoint.

A dial failure is returned to the caller: when the agent is configured as enabled, an unreachable endpoint is an unexpected condition (the daemon isn't running, or hasn't authenticated far enough to start the listener) and the caller should surface it as such rather than silently substituting config.

type Listener

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

Listener serves an agent backend over a platform transport (Unix domain socket or Windows named pipe). The shared Serve/Close logic lives here; the endpoint creation and teardown are platform-specific (listener_unix.go, listener_windows.go). Endpoint permissions are a hard invariant on both platforms: only the owning user may connect.

func NewListener

func NewListener(addr string, backend agent.ExtendedAgent) *Listener

NewListener returns a listener bound to addr (socket path or pipe name) that serves backend.

func (*Listener) Addr

func (l *Listener) Addr() string

Addr returns the configured endpoint address.

func (*Listener) Close

func (l *Listener) Close() error

Close shuts the endpoint down. Safe to call more than once.

func (*Listener) Serve

func (l *Listener) Serve(ctx context.Context) error

Serve creates the endpoint and accepts connections until ctx is cancelled, dispatching each to agent.ServeAgent in its own goroutine. Cancellation closes the endpoint, unblocks Accept, and returns nil — errors arising from the closed endpoint during shutdown are a clean stop, not a failure.

type Option

type Option func(*Backend)

Option configures a Backend.

func WithCacheTTL

func WithCacheTTL(d time.Duration) Option

WithCacheTTL sets the List cache window.

func WithEndpoint

func WithEndpoint(addr string) Option

WithEndpoint records the listen address for status reporting.

func WithReauthGate

func WithReauthGate(g ReauthGate) Option

WithReauthGate wires the token-lifecycle gate used to block Sign briefly during a re-authentication window.

func WithReauthTimeout

func WithReauthTimeout(d time.Duration) Option

WithReauthTimeout bounds how long Sign waits for re-auth to clear.

type ReauthGate

type ReauthGate interface {
	NeedsReauth() bool
}

ReauthGate lets the backend observe the daemon's token-lifecycle state so a Sign issued mid-reauth waits briefly rather than failing. *auth.LifecycleManager satisfies it.

type Service

type Service struct {
	Backend *Backend
	// contains filtered or unexported fields
}

Service bundles the agent backend with its transport listener(s) so the daemon lifecycle can treat the SSH agent as a single managed component. The backend survives token refreshes; only the listeners are (re)started. On Windows the agent may serve more than one endpoint — the dotvault pipe plus, when enabled, a Pageant-convention pipe — all sharing the one backend.

func NewService

func NewService(agentCfg config.AgentConfig, vc *vault.Client, kvMount, userPrefix, username string, gate ReauthGate) (*Service, error)

NewService resolves the endpoint(s), builds the key sources, and wires the backend + listeners. gate is the token-lifecycle gate (may be nil).

func (*Service) Endpoint

func (s *Service) Endpoint() string

Endpoint returns the primary resolved socket path / pipe name — the one a client connects to by default and the one `dotvault status` queries. Querying only the primary is deliberate: every endpoint shares the one backend, so the identities served on the Pageant pipe are identical to those on the primary — a second query would report the same thing.

func (*Service) Endpoints

func (s *Service) Endpoints() []string

Endpoints returns every endpoint the agent listens on (primary first), including the Pageant pipe on Windows when enabled.

func (*Service) Run

func (s *Service) Run(ctx context.Context)

Run serves the agent on every configured endpoint until ctx is cancelled, one supervised listener per endpoint. The listeners share the single backend (and its cached identities), so a token refresh does not bounce any of them.

type Source

type Source interface {
	// Name is a stable label for status and logging.
	Name() string

	// Type reports the source kind ("kv" or "vault-ca") for status output.
	Type() string

	// Identities returns the public keys/certs currently available. Sources
	// that have disappeared from Vault simply return fewer identities on the
	// next call — no restart required.
	Identities(ctx context.Context) ([]Identity, error)

	// Sign signs data with the private key matching key, if this source owns
	// it. matched is false (with a nil error) when the key belongs to another
	// source, so the backend can try the next one. The signature is obtained
	// at request time: KV sources read+parse+discard the private key; CA
	// sources ensure a fresh certificate and sign with the in-memory key.
	Sign(ctx context.Context, key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (sig *ssh.Signature, matched bool, err error)
}

Source is one configured origin of signing identities — a KV path prefix or a Vault SSH-CA role. The backend aggregates identities from every source for List and offers each source the chance to satisfy a Sign.

func NewSourcesFromConfig

func NewSourcesFromConfig(agentCfg config.AgentConfig, vc *vault.Client, kvMount, userPrefix, username string) ([]Source, error)

NewSourcesFromConfig builds the ordered key sources for the daemon from the agent config. kvMount/userPrefix/username come from the running config and identity resolution (userPrefix carries its trailing slash).

type SourceStatus

type SourceStatus struct {
	Name       string           `json:"name"`
	Type       string           `json:"type"`
	Error      string           `json:"error,omitempty"`
	Identities []IdentityStatus `json:"identities"`
}

SourceStatus reports one configured source's resolution result.

type Status

type Status struct {
	Endpoint string         `json:"endpoint"`
	Sources  []SourceStatus `json:"sources"`
}

Status is a serialisable snapshot of the agent's currently resolvable identities, per source. It is surfaced in the web dashboard (parallel to per-rule sync state) and printed by `dotvault status`.

Jump to

Keyboard shortcuts

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