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 ¶
- Variables
- func ResolveEndpoint(agentCfg config.AgentConfig) string
- type Backend
- func (b *Backend) Add(key agent.AddedKey) error
- func (b *Backend) Extension(extensionType string, contents []byte) ([]byte, error)
- func (b *Backend) List() ([]*agent.Key, error)
- func (b *Backend) Lock(passphrase []byte) error
- func (b *Backend) Remove(key ssh.PublicKey) error
- func (b *Backend) RemoveAll() error
- func (b *Backend) SetReauthGate(g ReauthGate)
- func (b *Backend) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error)
- func (b *Backend) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error)
- func (b *Backend) Signers() ([]ssh.Signer, error)
- func (b *Backend) Status(ctx context.Context) Status
- func (b *Backend) Unlock(passphrase []byte) error
- type Identity
- type IdentityStatus
- type Listener
- type Option
- type ReauthGate
- type Service
- type Source
- type SourceStatus
- type Status
Constants ¶
This section is empty.
Variables ¶
var ErrKeyNotFound = fmt.Errorf("no matching key")
ErrKeyNotFound is returned by Sign when no source owns the requested key.
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 ¶
NewBackend builds a backend over the given ordered sources.
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) 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.
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) Serve ¶
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 ¶
WithCacheTTL sets the List cache window.
func WithEndpoint ¶
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 ¶
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 ¶
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.
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`.