Documentation
¶
Overview ¶
Package agent implements the nssh credential agent daemon.
The agent is a background process that holds decrypted credentials in memory, providing secure credential access without requiring repeated passphrase entry. It communicates with nssh clients via a Unix domain socket using a JSON-based protocol.
Lifecycle ¶
The agent is spawned by Spawn or SpawnPIV when the vault is unlocked, and terminates automatically based on configurable timeouts:
- Idle timeout: terminates after period of inactivity (default 1h)
- Max lifetime: hard cap regardless of activity (default 24h)
- Manual lock: terminated via "nssh lock" command
Security Modes ¶
The agent supports multiple credential protection backends via the Provider interface:
- Software mode: passphrase-protected age identity with scrypt KDF
- PIV mode: YubiKey hardware-backed decryption (requires CGO build)
Protocol ¶
Clients communicate via JSON messages over the Unix socket. Operations include:
- OpDecrypt: decrypt age-encrypted credential data
- OpStatus: query agent status and remaining lifetime
- OpLock: terminate the agent session
Background Tasks ¶
The agent runs background goroutines for:
- Recording archival: monitors live session recordings and archives them
- Connection handling: serves client requests with concurrency limits
Example ¶
Typical usage pattern (handled by the unlock command):
// Spawn agent with software provider
if err := agent.Spawn(identity); err != nil {
return err
}
// Later, check if agent is running
if agent.IsRunning() {
// Use agent for decryption via session package
}
Package agent provides a transparent in-memory credential agent for nssh.
The agent holds decrypted age credentials in memory and communicates with nssh processes via a Unix domain socket. This replaces platform-specific keychain/keyring session caching with a simpler, cross-platform approach.
Index ¶
- Constants
- Variables
- func AddPIVKey(configDir string, key PIVKey) error
- func CreateSocket(path string) (net.Listener, error)
- func EncryptWithPIV(pubKey *ecdsa.PublicKey, plaintext []byte) ([]byte, error)
- func IsRunning() bool
- func IsSocketAlive(path string) bool
- func ListConnectedYubiKeys() ([]uint32, error)
- func LockPath() string
- func MarshalP256PublicKey(pub *ecdsa.PublicKey) []byte
- func PIVAvailable() bool
- func RemovePIVKey(configDir string, serial uint32) error
- func RemoveSocket(path string) error
- func Run(provider Provider, cfg RuntimeConfig) error
- func RunInBackground(ctx context.Context, provider Provider, cfg RuntimeConfig) (context.CancelFunc, <-chan struct{})
- func SavePIVKeystore(configDir string, ks *PIVKeystore) error
- func SavePIVKeystoreToPath(path string, ks *PIVKeystore) error
- func SetSocketPathForTest(path string) func()
- func SocketPath() string
- func Spawn(identitySecret *secret.Secret) error
- func SpawnPIV(pinSecret *secret.Secret) error
- func VerifyPeer(conn *net.UnixConn) error
- type Client
- type PIVKey
- type PIVKeystore
- type PIVMeta
- type PIVProvider
- type Provider
- type Request
- type Response
- type RuntimeConfig
- type SoftwareProvider
- type StatusInfo
Constants ¶
const ( DefaultIdleTimeout = 1 * time.Hour // No activity timeout DefaultMaxLifetime = 24 * time.Hour // Hard cap regardless of activity DefaultMaxSleep = 5 * time.Minute // Max sleep between deadline checks )
Default timeout values for the agent.
const ( OpHello = "hello" // Returns agent mode (e.g., "software") OpStatus = "status" // Returns session status (JSON StatusInfo) OpDecrypt = "decrypt" // Decrypts ciphertext, returns plaintext OpRecipient = "recipient" // Returns age public key for encryption OpLock = "lock" // Terminates the agent )
Operation constants for Request.Op field.
const ( ModeSoftware = string(mode.Software) ModePIV = string(mode.PIV) ModeFIDO2 = string(mode.FIDO2) ModeSecureEnclave = string(mode.SecureEnclave) )
ProviderMode constants for Provider.Mode() return values.
const ProtocolVersion = 1
ProtocolVersion is incremented when breaking changes are made to the protocol. Clients and agents must agree on version to communicate.
const SpawnTimeout = 10 * time.Second
SpawnTimeout is the maximum time to wait for the agent to signal readiness.
Variables ¶
var CompiledFeatures []string
CompiledFeatures lists optional security features compiled into this binary. The default build has no optional features (software mode is always available).
var ErrAgentNotRunning = errors.New("agent not running")
ErrAgentNotRunning indicates no agent is listening on the socket.
var ErrHardwareNotCompiled = errors.New("hardware support not compiled; build with -tags hardware")
ErrHardwareNotCompiled is returned when hardware provider functions are called but the binary was not built with the "hardware" build tag.
var ErrSecureEnclaveNotAvailable = errors.New("secure enclave not available; requires macOS with -tags secureenclave")
ErrSecureEnclaveNotAvailable is returned when Secure Enclave provider functions are called but the binary was not built with the required tags.
Functions ¶
func CreateSocket ¶
CreateSocket creates a Unix domain socket with secure permissions.
Security measures: - Parent directory created with 0700 permissions - Socket created with restrictive umask (0077) - flock-based locking prevents TOCTOU races during creation - Stale sockets are removed before creating new ones
func EncryptWithPIV ¶
EncryptWithPIV stub for non-hardware builds.
func IsRunning ¶
func IsRunning() bool
IsRunning checks if an agent is currently running and responsive.
func IsSocketAlive ¶
IsSocketAlive checks if an agent is listening on the socket.
func ListConnectedYubiKeys ¶
ListConnectedYubiKeys stub for non-hardware builds.
func LockPath ¶
func LockPath() string
LockPath returns the path to the socket creation lock file. This lock prevents TOCTOU races during socket creation.
func MarshalP256PublicKey ¶
MarshalP256PublicKey stub for non-hardware builds.
func PIVAvailable ¶
func PIVAvailable() bool
PIVAvailable reports whether PIV support is compiled in. Returns false in non-hardware builds.
func RemovePIVKey ¶
RemovePIVKey stub for non-hardware builds.
func RemoveSocket ¶
RemoveSocket removes the agent socket file. Safe to call even if socket doesn't exist.
func Run ¶
func Run(provider Provider, cfg RuntimeConfig) error
Run starts the agent daemon with the given provider and configuration. The agent listens on a Unix socket and handles decrypt requests until: - Idle timeout expires (no activity for IdleTimeout duration) - Max lifetime expires (regardless of activity) - Lock command received from client - SIGTERM/SIGINT/SIGHUP signal received
This function blocks until the agent shuts down.
func RunInBackground ¶
func RunInBackground(ctx context.Context, provider Provider, cfg RuntimeConfig) (context.CancelFunc, <-chan struct{})
RunInBackground starts the agent in a background context. Returns a cancel function to stop the agent and a channel that closes when done.
func SavePIVKeystore ¶
func SavePIVKeystore(configDir string, ks *PIVKeystore) error
SavePIVKeystore stub for non-hardware builds.
func SavePIVKeystoreToPath ¶
func SavePIVKeystoreToPath(path string, ks *PIVKeystore) error
SavePIVKeystoreToPath stub for non-hardware builds.
func SetSocketPathForTest ¶
func SetSocketPathForTest(path string) func()
SetSocketPathForTest overrides the socket path for testing. Returns a cleanup function to restore the original.
func SocketPath ¶
func SocketPath() string
SocketPath returns the platform-appropriate Unix socket path for the agent.
On Linux: $XDG_RUNTIME_DIR/nssh.sock (fallback: /tmp/nssh-{uid}.sock) On macOS: $TMPDIR/nssh.sock (per-user directory, cleared on reboot)
On Linux with systemd, XDG_RUNTIME_DIR is cleared on logout, effectively terminating the session. On macOS, both the socket directory and agent process persist across logout/login - only reboot clears them.
func Spawn ¶
Spawn starts a new agent daemon in the background with the given identity.
The agent is started as a new session leader (via Setsid) so it survives terminal close. On Linux with systemd, the agent dies on logout (systemd kills user session processes) and XDG_RUNTIME_DIR is cleared. On macOS, the agent persists across logout/login and only terminates on reboot, idle timeout, max lifetime, or explicit lock command.
The identity is passed to the agent via an inherited pipe (fd 3). The agent signals readiness on another pipe (fd 4) by writing "ok\n" on success or "err:message\n" on failure.
After Spawn returns successfully, the identity secret has been transferred to the agent and should be destroyed by the caller.
func SpawnPIV ¶
SpawnPIV starts a new agent daemon in PIV mode.
Instead of passing the decrypted identity like Spawn(), this passes the YubiKey PIN. The agent uses the PIN to unlock the YubiKey and decrypt the age identity stored in age.key.piv.
The PIN is passed via the same pipe mechanism (fd 3) as the identity in software mode. The agent determines what to expect based on the configured security mode.
func VerifyPeer ¶
VerifyPeer checks that the connecting process has the same UID as this process. This prevents other users from connecting to the agent socket.
On Linux, we use SO_PEERCRED to get the peer's credentials.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client connects to a running agent and sends requests.
func Connect ¶
Connect establishes a connection to the running agent. Returns ErrAgentNotRunning if no agent is listening.
func (*Client) Decrypt ¶
Decrypt sends ciphertext to the agent for decryption. Returns the plaintext on success.
func (*Client) Hello ¶
Hello sends a hello request to verify the agent is responsive. Returns the agent's security mode (e.g., "software").
func (*Client) Status ¶
func (c *Client) Status() (*StatusInfo, error)
Status returns session status including timing information.
type PIVKey ¶
type PIVKey struct {
Serial uint32 `json:"serial"`
SlotKey uint32 `json:"slot_key"`
PublicKey []byte `json:"public_key"`
Label string `json:"label,omitempty"`
Enrolled string `json:"enrolled,omitempty"`
Identity []byte `json:"identity"`
}
PIVKey represents a single enrolled YubiKey.
type PIVKeystore ¶
PIVKeystore is the v2 multi-key format for piv.json.
func LoadPIVKeystore ¶
func LoadPIVKeystore(configDir string) (*PIVKeystore, error)
LoadPIVKeystore stub for non-hardware builds.
type PIVMeta ¶
type PIVMeta struct {
Serial uint32 `json:"serial"`
SlotKey uint32 `json:"slot_key"`
PublicKey []byte `json:"public_key"`
}
PIVMeta is the v1 single-key format (for migration). Defined in stub for type compatibility with init code.
type PIVProvider ¶
type PIVProvider struct{}
PIVProvider stub for type compatibility.
func NewPIVProvider ¶
func NewPIVProvider(configDir string, pinPrompt func() (string, error)) (*PIVProvider, error)
NewPIVProvider returns an error indicating hardware support is not compiled. To use PIV/YubiKey hardware tokens, build with: go build -tags hardware
func (*PIVProvider) Close ¶
func (p *PIVProvider) Close() error
Close is a no-op (stub implementation).
func (*PIVProvider) Decrypt ¶
func (p *PIVProvider) Decrypt(ciphertext []byte) ([]byte, error)
Decrypt returns an error (stub implementation).
func (*PIVProvider) Mode ¶
func (p *PIVProvider) Mode() string
Mode returns "piv" (stub implementation).
func (*PIVProvider) Recipient ¶
func (p *PIVProvider) Recipient() string
Recipient returns empty string (stub implementation).
type Provider ¶
type Provider interface {
// Mode returns the security mode identifier.
// Examples: "software", "piv", "fido2", "secureenclave"
Mode() string
// Decrypt decrypts age-encrypted ciphertext using the provider's key.
// Returns the plaintext on success.
Decrypt(ciphertext []byte) ([]byte, error)
// Recipient returns the age-compatible public key string for encryption.
// This is used when encrypting new credentials for the vault.
Recipient() string
// Close zeroizes secrets and releases any resources held by the provider.
// Must be called when the agent shuts down.
Close() error
}
Provider abstracts cryptographic operations for different security modes. The agent delegates decryption to a Provider, enabling software-only and hardware-backed security modes.
Implementations:
- softwareProvider: Default, holds age X25519 identity in memory (CGO=0 compatible)
- pivProvider: PIV/YubiKey hardware token (requires hardware build tag + CGO)
- fido2Provider: FIDO2/WebAuthn hardware key (requires hardware build tag + CGO)
- secureEnclaveProvider: macOS Secure Enclave (requires secureenclave build tag + CGO)
func NewFIDO2Provider ¶
NewFIDO2Provider returns an error indicating hardware support is not compiled. To use FIDO2/WebAuthn hardware keys, build with: go build -tags hardware
func NewSecureEnclaveProvider ¶
NewSecureEnclaveProvider returns an error indicating Secure Enclave support is not available. To use macOS Secure Enclave, build on macOS with: go build -tags secureenclave
type Request ¶
type Request struct {
Version int `json:"v"` // Protocol version (required)
ID string `json:"id,omitempty"` // Request ID for log correlation (optional)
Op string `json:"op"` // Operation: "hello", "decrypt", "recipient", "lock"
Data []byte `json:"data,omitempty"` // Ciphertext for decrypt operation
}
Request represents a message from client to agent.
type Response ¶
type Response struct {
ID string `json:"id,omitempty"` // Echoes request ID for correlation
OK bool `json:"ok"` // true if operation succeeded
Data []byte `json:"data,omitempty"` // Result data (mode string, plaintext, or recipient)
Err string `json:"err,omitempty"` // Error message if OK is false
}
Response represents a message from agent to client.
type RuntimeConfig ¶
type RuntimeConfig struct {
Agent *config.AgentConfig // Timeout settings from config file
Archive *config.SessionArchiveConfig // Archive settings from config file
Logger *slog.Logger
ReadyPipe *os.File // Optional: pipe to signal readiness after socket creation
MaxSleep time.Duration // Max sleep between deadline checks (0 = default 5m)
Clock clock // Optional clock (tests can inject fake); defaults to realClock
RecordingDir string // Directory containing live .cast recordings
}
RuntimeConfig holds runtime configuration for the agent daemon. Agent settings come from config.AgentConfig; runtime-only fields are separate.
func DefaultRuntimeConfig ¶
func DefaultRuntimeConfig() RuntimeConfig
DefaultRuntimeConfig returns a RuntimeConfig with default values.
type SoftwareProvider ¶
type SoftwareProvider struct {
// contains filtered or unexported fields
}
SoftwareProvider implements Provider using an in-memory age X25519 identity. This is the default provider, compatible with CGO=0 builds.
func NewSoftwareProvider ¶
func NewSoftwareProvider(identity *age.X25519Identity) *SoftwareProvider
NewSoftwareProvider creates a new software provider with the given identity.
func (*SoftwareProvider) Close ¶
func (p *SoftwareProvider) Close() error
Close zeroizes the identity. After calling Close, the provider cannot be used.
Note: Go's age library doesn't expose the raw key bytes for zeroing, so we set the reference to nil to allow garbage collection. The actual memory zeroing happens when the identity was created from memguard-protected memory in the spawn process.
func (*SoftwareProvider) Decrypt ¶
func (p *SoftwareProvider) Decrypt(ciphertext []byte) ([]byte, error)
Decrypt decrypts age-encrypted ciphertext using the X25519 identity.
func (*SoftwareProvider) Recipient ¶
func (p *SoftwareProvider) Recipient() string
Recipient returns the age public key string for encryption.
type StatusInfo ¶
type StatusInfo struct {
Mode string `json:"mode"` // Security mode (e.g., "software")
IdleTimeout int64 `json:"idle_timeout"` // Configured idle timeout in seconds
MaxLifetime int64 `json:"max_lifetime"` // Configured max lifetime in seconds
RemainingLife int64 `json:"remaining_life"` // Seconds until max lifetime expires
RemainingIdle int64 `json:"remaining_idle"` // Seconds until idle timeout (approximate)
}
StatusInfo is returned by the status operation.