agent

package
v0.2.3 Latest Latest
Warning

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

Go to latest
Published: Dec 16, 2025 License: GPL-3.0 Imports: 30 Imported by: 0

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

View Source
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.

View Source
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.

View Source
const (
	ModeSoftware      = string(mode.Software)
	ModePIV           = string(mode.PIV)
	ModeFIDO2         = string(mode.FIDO2)
	ModeSecureEnclave = string(mode.SecureEnclave)
)

ProviderMode constants for Provider.Mode() return values.

View Source
const ProtocolVersion = 1

ProtocolVersion is incremented when breaking changes are made to the protocol. Clients and agents must agree on version to communicate.

View Source
const SpawnTimeout = 10 * time.Second

SpawnTimeout is the maximum time to wait for the agent to signal readiness.

Variables

View Source
var CompiledFeatures []string

CompiledFeatures lists optional security features compiled into this binary. The default build has no optional features (software mode is always available).

View Source
var ErrAgentNotRunning = errors.New("agent not running")

ErrAgentNotRunning indicates no agent is listening on the socket.

View Source
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.

View Source
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 AddPIVKey

func AddPIVKey(configDir string, key PIVKey) error

AddPIVKey stub for non-hardware builds.

func CreateSocket

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

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

func EncryptWithPIV(pubKey *ecdsa.PublicKey, plaintext []byte) ([]byte, error)

EncryptWithPIV stub for non-hardware builds.

func IsRunning

func IsRunning() bool

IsRunning checks if an agent is currently running and responsive.

func IsSocketAlive

func IsSocketAlive(path string) bool

IsSocketAlive checks if an agent is listening on the socket.

func ListConnectedYubiKeys

func ListConnectedYubiKeys() ([]uint32, error)

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

func MarshalP256PublicKey(pub *ecdsa.PublicKey) []byte

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

func RemovePIVKey(configDir string, serial uint32) error

RemovePIVKey stub for non-hardware builds.

func RemoveSocket

func RemoveSocket(path string) error

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

func Spawn(identitySecret *secret.Secret) error

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

func SpawnPIV(pinSecret *secret.Secret) error

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

func VerifyPeer(conn *net.UnixConn) error

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

func Connect() (*Client, error)

Connect establishes a connection to the running agent. Returns ErrAgentNotRunning if no agent is listening.

func (*Client) Close

func (c *Client) Close() error

Close closes the connection to the agent.

func (*Client) Decrypt

func (c *Client) Decrypt(ciphertext []byte) ([]byte, error)

Decrypt sends ciphertext to the agent for decryption. Returns the plaintext on success.

func (*Client) Hello

func (c *Client) Hello() (string, error)

Hello sends a hello request to verify the agent is responsive. Returns the agent's security mode (e.g., "software").

func (*Client) Lock

func (c *Client) Lock() error

Lock sends a lock command to terminate the agent.

func (*Client) Recipient

func (c *Client) Recipient() (string, error)

Recipient returns the agent's age public key for encryption.

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

type PIVKeystore struct {
	Version int      `json:"version"`
	Keys    []PIVKey `json:"keys"`
}

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

func NewFIDO2Provider() (Provider, error)

NewFIDO2Provider returns an error indicating hardware support is not compiled. To use FIDO2/WebAuthn hardware keys, build with: go build -tags hardware

func NewSecureEnclaveProvider

func NewSecureEnclaveProvider() (Provider, error)

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) Mode

func (p *SoftwareProvider) Mode() string

Mode returns "software".

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.

Jump to

Keyboard shortcuts

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