Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .justfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ _help:

# Run all unit tests using nextest.
test:
cargo nextest run --future-incompat-report
cargo nextest run --workspace --all-targets --future-incompat-report
cargo nextest run -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml --future-incompat-report

# Run the fuzz tests against the wireline protocol.
fuzz:
Expand All @@ -24,13 +25,15 @@ fmt:

# Run the same checks we run in CI. Requires nightly.
ci: test fmt
cargo clippy --all-targets
cargo clippy --workspace --all-targets
cargo clippy -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml
cargo test --doc
cargo test --doc -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml

# Install required tools
setup:
brew tap ceejbot/tap
brew install cargo-nextest tomato semver-bump
brew install cargo-nextest tomato semver-bump cargo-llvm-cov
rustup install nightly

# Tag a new version for release.
Expand Down
76 changes: 76 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

zerolease is a lightweight, agent-aware credential vault with lease-based access control, written in Rust. It's designed for AI agent orchestration environments where untrusted tools need time-bounded, scope-limited access to credentials. Supports deployment on developer laptops (Unix domain socket + OS keychain), QEMU VMs (TCP + token auth), and VM-isolated environments (vsock + AWS KMS).

**Status**: Early development — core traits, types, and multiple backend implementations are functional. The workspace includes storage backends for rusqlite, PostgreSQL, and AWS Secrets Manager.

## Build Commands

```bash
cargo build # Build workspace (core + rusqlite store)
cargo test --workspace # Run all workspace tests
cargo clippy --workspace # Lint all workspace members
cargo fmt # Format

# Excluded crate (sqlx/rusqlite conflict):
cargo build --manifest-path crates/zerolease-store-postgres/Cargo.toml
cargo clippy --manifest-path crates/zerolease-store-postgres/Cargo.toml --all-targets
```

Requires Rust edition 2024.

## Workspace Structure

| Crate | Location | In workspace? |
|-------|----------|---------------|
| `zerolease` (core) | `.` | yes |
| `zerolease-provider` | `crates/zerolease-provider` | yes |
| `zerolease-store-rusqlite` | `crates/zerolease-store-rusqlite` | yes |
| `zerolease-store-aws-sm` | `crates/zerolease-store-aws-sm` | yes |
| `zerolease-store-postgres` | `crates/zerolease-store-postgres` | **excluded** (sqlx conflict) |

The postgres crate is excluded because sqlx and rusqlite both link `libsqlite3-sys`. Build/test it separately with `--manifest-path`.

## Architecture

The vault is a generic struct `Vault<K, S, A>` parameterized over three backend traits, allowing compile-time selection of deployment configuration:

| Trait | Purpose | Implementations |
|-------|---------|-----------------|
| `KeySource` | Master key (DEK) management | OS keychain, AWS KMS, env var |
| `SecretStore` | Encrypted secret persistence | rusqlite, PostgreSQL, AWS Secrets Manager |
| `AuditLog` | Append-only event log | `TracingAuditLog` (core), rusqlite, PostgreSQL |

Transport is a separate abstraction (`VaultListener`/`VaultConnector`) over Unix domain sockets, TCP, and vsock. Authentication is pluggable via the `Authenticator` trait.

### Request Flow

Agent -> Transport -> Handshake (ClientHello/ServerHello) -> Authenticator (PeerIdentity + token -> ConnectionIdentity) -> Vault -> PolicyEngine (deny-by-default, first-match) -> SecretStore (encrypted blob) -> decrypt with DEK -> create Lease + LeaseGuard -> AuditLog -> return LeaseGrant to agent.

### Key Design Decisions

- **Newtype IDs**: `SecretId`, `AgentId`, `LeaseId`, `SecretName`, `DomainScope` are all newtypes preventing accidental misuse at compile time. All UUID-based IDs use v7 (time-ordered).
- **Zeroize-on-drop**: Secret values use `SecretString`/`Zeroize`. `LeaseGuard` is not Clone, not Serialize, and redacts in Debug output.
- **Envelope encryption**: KMS-backed deployments use a local DEK encrypted by KMS, avoiding a KMS round-trip per secret operation.
- **DomainScope restriction**: Credentials are scoped to target domains (exact match, wildcard subdomain `*.example.com`, or localhost:port).
- **Policy model**: Deny-by-default, flat grant list, first-match-wins. Designed for auditability.
- **Transport↔Auth separation**: Transports provide `PeerIdentity` (UID/PID, CID, or token hash). The `Authenticator` maps this to `ConnectionIdentity` (role + agent binding). TCP transports include a bearer token in `ClientHello`.
- **Storage↔Audit decoupling**: `SecretStore` and `AuditLog` are independent — pick each backend separately. AWS SM provides only `SecretStore`; pair with `TracingAuditLog`.

### Feature Flags (core crate)

- `vsock` — enables tokio-vsock for VM communication (Linux only)
- `kms` — enables AWS KMS key source

### Code Quality Principles

- Idiomatic Rust. Clippy clean. Experienced Rust developers should feel at home.
- Prefer well-tested, well-established dependencies from known community members.
- Tidy, efficient code. Well-named variables and functions. Readable by humans and agents.
- Memory and CPU efficient — no needless clones. Compatible with the zeroclaw philosophy.
- Use Rust types to prevent bugs (newtypes, enums, exhaustive matching).
- A little macro-writing goes a long way for readability (`col!`, `parse_params!`, `json_response!`).
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = [".", "crates/zerolease-provider", "crates/zerolease-store-rusqlite", "crates/zerolease-store-aws-sm"]
members = [".", "crates/zerolease-provider", "crates/zerolease-store-rusqlite", "crates/zerolease-store-aws-sm", "crates/zerolease-agent"]
# zerolease-store-postgres excluded from default workspace due to sqlx v0.8
# libsqlite3-sys conflict with rusqlite. Build/test it separately:
# cargo check -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml
Expand Down Expand Up @@ -58,6 +58,7 @@ chrono.workspace = true
secrecy.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2 = "0.10"
thiserror.workspace = true
tokio.workspace = true
tracing = "0.1"
Expand Down
120 changes: 69 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,45 @@ When AI agents use tools that need credentials — API tokens, SSH keys, databas

## Design

The vault is a single Rust process that agents connect to over Unix domain sockets (on developer machines) or vsock (in Firecracker/QEMU VMs). Credentials never leave the vault as plaintext over a network — the transport is local to the host or hypervisor.
The vault is a Rust library that agents connect to over Unix domain sockets (developer machines), TCP with token auth (QEMU VMs), or vsock (Firecracker). Credentials never leave the vault as plaintext over a network — the transport is local to the host or hypervisor.

**Encryption.** Secrets are encrypted at rest using `AES-256-GCM` or `XChaCha20-Poly1305` (configurable per secret, with algorithm migration support). The data encryption key is managed by a pluggable key source: OS keychain for developer machines, AWS KMS for production, or an environment variable for CI.

**Policy.** Access is deny-by-default. A flat list of grant rules specifies which agents can access which secrets for which domains. First match wins. The policy format is intentionally simple — easier to audit than a policy language.

**Leases.** Every credential access goes through a lease. Leases have a TTL, an optional use count, and a list of allowed target domains. The vault tracks active leases in memory, enforces per-agent caps, and garbage-collects expired ones. Secret values are zeroized from memory when the lease guard is dropped.

**Authentication.** Connections are authenticated via a pluggable `Authenticator` trait that maps transport-level peer identity to roles. Three roles exist: Admin (full access), Agent (bound to a single identity, can only use leases), and Orchestrator (trusted to assert agent identity per request, for systems like Slack bots acting on behalf of multiple users).
**Authentication.** Connections are authenticated via a pluggable `Authenticator` trait that maps transport-level peer identity to roles. Three roles exist: Admin (full access), Agent (bound to a single identity, can only use leases), and Orchestrator (trusted to assert agent identity per request). TCP transports present a bearer token in the handshake; UDS/vsock rely on OS-level identity.

**Audit.** Every lease grant, secret access, revocation, and policy denial is emitted as a structured `tracing` event (observable via any tracing subscriber) and optionally persisted to a queryable SQLite audit log.
**Audit.** Every lease grant, secret access, revocation, and policy denial is logged. The core crate includes `TracingAuditLog` (emits structured `tracing` events for external log aggregation). Queryable backends are available in the store crates.

## Workspace

zerolease is a Cargo workspace. The core crate defines traits; storage and provider crates are chosen at compile time.

| Crate | Purpose |
|-------|---------|
| **zerolease** | Core: `Vault`, traits (`SecretStore`, `AuditLog`, `KeySource`), transports, policy engine |
| **zerolease-store-rusqlite** | SQLite storage via rusqlite — `SecretStore` + `AuditLog` |
| **zerolease-store-postgres** | PostgreSQL storage via sqlx — `SecretStore` + `AuditLog` |
| **zerolease-store-aws-sm** | AWS Secrets Manager — `SecretStore` only (pair with `TracingAuditLog`) |
| **zerolease-provider** | `CredentialProvider` trait for AI agent tool integration |

Pick a store crate and an audit backend independently:

- **Developer laptop:** rusqlite store + `RusqliteAuditLog` (single file, zero config)
- **Cloud VMs:** AWS SM store + `TracingAuditLog` (logs to stdout → CloudWatch)
- **Shared infra:** PostgreSQL store + `PostgresAuditLog`

## Transports

| Transport | Use case | Identity source |
|-----------|----------|-----------------|
| **Unix domain socket** | Developer machines, local processes | OS peer credentials (UID/PID) |
| **TCP + token** | QEMU VMs via host-forwarded ports | Bearer token in `ClientHello` handshake |
| **vsock** | Firecracker/QEMU via virtio | Guest CID (Linux only, feature `vsock`) |

TCP listeners bind to `127.0.0.1` only. The `TokenAuthenticator` maps pre-registered tokens to connection identities. Raw tokens are never stored — only SHA-256 hashes.

## Quick start

Expand All @@ -32,80 +60,70 @@ export ZEROLEASE_KEY=$(openssl rand -hex 32)

# Run the example (direct vault API, no server)
cargo run --example basic_vault

# Or start the standalone server
cargo run -- --socket /tmp/zerolease.sock --db secrets.db --audit-db audit.db
```

The example stores a secret, requests a lease, accesses the credential through the lease, and demonstrates domain restriction.

## Feature flags

| Flag | Default | What it enables |
| ---------- | ------- | ----------------------------------------------------- |
| `sqlite` | Yes | SQLite secret store and audit log |
| `postgres` | No | PostgreSQL secret store |
| `kms` | No | AWS KMS envelope encryption key source |
| `vsock` | No | vsock transport for Firecracker/QEMU VMs (Linux only) |

## Building

Requires Rust edition 2024.

```
cargo build # default features (SQLite)
cargo build --features postgres # with PostgreSQL support
cargo build --features kms # with AWS KMS support
cargo build --features vsock # with vsock transport (Linux only)
cargo test # run tests (116 default)
cargo doc --open # browse API documentation
```bash
cargo build # core + rusqlite store
cargo test --workspace # run workspace tests
cargo clippy --workspace # lint

# Excluded crate (built separately due to sqlx/rusqlite conflict):
cargo build --manifest-path crates/zerolease-store-postgres/Cargo.toml
```

## Testing
### Feature flags (core crate)

The test suite covers unit tests, integration tests, security tests, and fuzz targets.
| Flag | Default | What it enables |
|---------|---------|-----------------|
| `vsock` | No | vsock transport for Firecracker/QEMU VMs (Linux only) |
| `kms` | No | AWS KMS envelope encryption key source |

## Testing

```bash
# Default test suite (116 tests)
cargo test
# Workspace tests
cargo test --workspace

# PostgreSQL integration tests (requires a running Postgres)
createdb zerolease_test
cargo test --features postgres store::postgres::tests -- --ignored --test-threads=1
# PostgreSQL integration tests (requires running Postgres)
cargo test --manifest-path crates/zerolease-store-postgres/Cargo.toml \
--run-ignored ignored-only --test-threads=1

# AWS KMS integration tests (requires AWS credentials)
cargo test --features kms keysource::kms::tests -- --ignored
# AWS Secrets Manager tests (requires credentials + IAM permissions)
ZEROLEASE_SM_TEST_PREFIX=zerolease_test_ \
cargo test -p zerolease-store-aws-sm --run-ignored ignored-only --test-threads=1

# OS keychain integration test (requires macOS Keychain or Linux secret-service)
cargo test keysource::keychain -- --ignored
# AWS KMS integration tests
cargo test -p zerolease --features kms -E 'test(keysource::kms)' --run-ignored ignored-only

# Fuzz targets (requires nightly)
cargo +nightly fuzz run fuzz_read_frame -- -max_total_time=60
cargo +nightly fuzz run fuzz_protocol_deser -- -max_total_time=60
cargo +nightly fuzz run fuzz_domain_scope -- -max_total_time=60
# OS keychain integration test
cargo test -p zerolease keysource::keychain -- --ignored
```

## Wire protocol

The protocol is JSON over length-prefixed frames (4-byte big-endian length + payload). Each connection begins with a version handshake. Requests carry a UUID v7 identifier for correlation. Eight methods are supported: `store_secret`, `request_lease`, `access_secret`, `revoke_lease`, `revoke_all_for_agent`, `list_secrets`, `renew_lease`, `delete_secret`.

See the module documentation (`cargo doc --open`) for protocol details and type definitions.
JSON over length-prefixed frames (4-byte big-endian length + payload). Each connection begins with a `ClientHello`/`ServerHello` handshake (TCP clients include a bearer token). Requests carry a UUID v7 identifier for correlation. Eight methods: `store_secret`, `request_lease`, `access_secret`, `revoke_lease`, `revoke_all_for_agent`, `list_secrets`, `renew_lease`, `delete_secret`.

## Security

The vault has been through an adversarial security audit with all findings resolved:

- Secret material is zeroized on drop (`Zeroize`, `SecretString`, `Zeroizing<Vec<u8>>`)
- Decryption errors are generic (no information leakage about failure cause)
- Secret material zeroized on drop (`Zeroize`, `SecretString`, `Zeroizing<Vec<u8>>`)
- Decryption errors are generic (no information leakage)
- Domain scope matching rejects edge cases (empty subdomains, path traversal)
- Policy engine is deny-by-default; empty prefix patterns are warned
- Lease renewal is capped at 24 hours; per-agent lease count is capped
- DEK rotation is atomic (database transaction)
- SQL injection is prevented by parameterized queries (tested explicitly)
- Lease renewal capped at 24 hours; per-agent lease count capped
- SQL injection prevented by parameterized queries
- Debug impls redact secret material
- Role-based access control prevents agents from calling admin operations
- Agent identity is bound at the transport level, not self-asserted
- Wire protocol fuzz-tested (~3.5 million executions, zero crashes)
- Agent identity bound at transport level, not self-asserted
- Auth tokens stored as SHA-256 hashes, never in plaintext
- TCP listener binds localhost only

## Status

Early development. Trait definitions and core types are stable. Concrete backend implementations are functional. Integration-level documentation and a standalone server binary are planned.

## License

Expand Down
38 changes: 38 additions & 0 deletions crates/zerolease-agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[package]
name = "zerolease-agent"
version = "0.1.0"
description = "VM-side agent: credential provisioner, lease-aware proxy, git credential helper"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true

[[bin]]
name = "zerolease-agent"
path = "src/main.rs"

[dependencies]
chrono.workspace = true
clap = { version = "4", features = ["derive", "env"] }
serde.workspace = true
serde_json.workspace = true
tokio = { workspace = true, features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid.workspace = true
zerolease = { workspace = true, default-features = false }

[dev-dependencies]
tempfile = "3"

[target.'cfg(unix)'.dependencies]
nix = { version = "0.29", features = ["process"] }

# Can't use workspace lints because the provisioner needs unsafe for env::set_var.
[lints.rust]
unsafe_code = "allow"
future_incompatible = { level = "deny", priority = 1 }
rust_2018_idioms = { level = "warn", priority = 2 }

[lints.clippy]
unwrap_used = "deny"
Loading
Loading