Skip to content

Commit d60af43

Browse files
authored
feat: TCP transport, token auth, lease-aware proxy, and VM agent (#5)
## Summary Adds TCP transport with token-based authentication, a lease-aware network proxy, and a VM-side agent binary — the complete infrastructure for deploying zerolease in QEMU VM environments where AI coding agents need credential access. ### TCP Transport + Token Authentication - `PeerIdentity::Tcp` with peer address + SHA-256 token hash (never raw token) - `ClientHello.token` field (backward-compatible — UDS/vsock omit it) - `TcpListener` (localhost-only) + `TcpConnector` - `TokenAuthenticator`: reference impl mapping pre-registered tokens to identities - `VaultClient::connect_with_token()` ### Lease-Aware Proxy (security-audited) An HTTPS CONNECT proxy that makes lease revocation mean "network access cut off": - **Explicit mode** (port 8080): Parses HTTP CONNECT, validates domain against lease state - **Transparent mode** (port 8443): Extracts domain from TLS SNI for tools ignoring `HTTPS_PROXY` - Active lease → bidirectional TCP tunnel (time-bounded to lease expiry) - Expired/unknown/revoked → 403 Forbidden Security hardening from adversarial audit: - SSRF prevention: DNS resolve + private IP blocklist (blocks 169.254.169.254, 10.x, etc.) - Port restriction: only 443/8443 allowed - DoS prevention: bounded request line (8 KiB) and header count (64) - Generic 502 responses (no internal error leakage) - Case-normalized domain matching - Tunnel timeout derived from lease expiry ### VM Agent Binary (`zerolease-agent`) Three subcommands in a single binary: - **provision**: Acquires credentials, writes env file + config files + lease state, exits. Vault token dies with this process (never enters agent env). - **proxy**: Long-running lease-aware proxy (described above) - **credential-fill**: Git credential helper with per-request domain validation Credential injection via manifest with three mechanisms: - `env`: Environment variables (GITHUB_TOKEN, NPM_TOKEN, etc.) - `file`: Config files from templates with `${SECRET}` expansion (mode 0600) - `git_credential`: Git credential helper mapping (per-request domain validation) ### Documentation - Deployment architecture doc with ASCII diagram + Mermaid sequence chart - CLI README with use cases: git HTTPS/SSH, gh, Fastly, AWS OIDC, npm, pip, cargo, Docker, databases - Updated main README and CLAUDE.md ## Test plan - [x] 100 tests across workspace, all passing - [x] TCP transport: bind, connect, frame round-trip, token handshake accept/reject - [x] TokenAuthenticator: register, authenticate, revoke, reject unknown - [x] CONNECT proxy: 403 for denied/expired, 502 for private IPs, 400 for bad hostnames, 403 for disallowed ports - [x] SNI extraction: synthetic ClientHello, missing SNI, truncated, non-TLS - [x] Lease state: active/expired/prune, atomic write/read - [x] Manifest: full format, git host map, unknown mechanism rejection - [x] Config writer: template expansion, tilde expansion, permissions - [x] Security audit: all Critical/High findings addressed, Medium findings addressed - [ ] CI green
1 parent ee5ce2f commit d60af43

28 files changed

Lines changed: 3058 additions & 113 deletions

File tree

.justfile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ _help:
33

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

89
# Run the fuzz tests against the wireline protocol.
910
fuzz:
@@ -24,13 +25,15 @@ fmt:
2425

2526
# Run the same checks we run in CI. Requires nightly.
2627
ci: test fmt
27-
cargo clippy --all-targets
28+
cargo clippy --workspace --all-targets
29+
cargo clippy -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml
2830
cargo test --doc
31+
cargo test --doc -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml
2932

3033
# Install required tools
3134
setup:
3235
brew tap ceejbot/tap
33-
brew install cargo-nextest tomato semver-bump
36+
brew install cargo-nextest tomato semver-bump cargo-llvm-cov
3437
rustup install nightly
3538

3639
# Tag a new version for release.

CLAUDE.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
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).
8+
9+
**Status**: Early development — core traits, types, and multiple backend implementations are functional. The workspace includes storage backends for rusqlite, PostgreSQL, and AWS Secrets Manager.
10+
11+
## Build Commands
12+
13+
```bash
14+
cargo build # Build workspace (core + rusqlite store)
15+
cargo test --workspace # Run all workspace tests
16+
cargo clippy --workspace # Lint all workspace members
17+
cargo fmt # Format
18+
19+
# Excluded crate (sqlx/rusqlite conflict):
20+
cargo build --manifest-path crates/zerolease-store-postgres/Cargo.toml
21+
cargo clippy --manifest-path crates/zerolease-store-postgres/Cargo.toml --all-targets
22+
```
23+
24+
Requires Rust edition 2024.
25+
26+
## Workspace Structure
27+
28+
| Crate | Location | In workspace? |
29+
|-------|----------|---------------|
30+
| `zerolease` (core) | `.` | yes |
31+
| `zerolease-provider` | `crates/zerolease-provider` | yes |
32+
| `zerolease-store-rusqlite` | `crates/zerolease-store-rusqlite` | yes |
33+
| `zerolease-store-aws-sm` | `crates/zerolease-store-aws-sm` | yes |
34+
| `zerolease-store-postgres` | `crates/zerolease-store-postgres` | **excluded** (sqlx conflict) |
35+
36+
The postgres crate is excluded because sqlx and rusqlite both link `libsqlite3-sys`. Build/test it separately with `--manifest-path`.
37+
38+
## Architecture
39+
40+
The vault is a generic struct `Vault<K, S, A>` parameterized over three backend traits, allowing compile-time selection of deployment configuration:
41+
42+
| Trait | Purpose | Implementations |
43+
|-------|---------|-----------------|
44+
| `KeySource` | Master key (DEK) management | OS keychain, AWS KMS, env var |
45+
| `SecretStore` | Encrypted secret persistence | rusqlite, PostgreSQL, AWS Secrets Manager |
46+
| `AuditLog` | Append-only event log | `TracingAuditLog` (core), rusqlite, PostgreSQL |
47+
48+
Transport is a separate abstraction (`VaultListener`/`VaultConnector`) over Unix domain sockets, TCP, and vsock. Authentication is pluggable via the `Authenticator` trait.
49+
50+
### Request Flow
51+
52+
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.
53+
54+
### Key Design Decisions
55+
56+
- **Newtype IDs**: `SecretId`, `AgentId`, `LeaseId`, `SecretName`, `DomainScope` are all newtypes preventing accidental misuse at compile time. All UUID-based IDs use v7 (time-ordered).
57+
- **Zeroize-on-drop**: Secret values use `SecretString`/`Zeroize`. `LeaseGuard` is not Clone, not Serialize, and redacts in Debug output.
58+
- **Envelope encryption**: KMS-backed deployments use a local DEK encrypted by KMS, avoiding a KMS round-trip per secret operation.
59+
- **DomainScope restriction**: Credentials are scoped to target domains (exact match, wildcard subdomain `*.example.com`, or localhost:port).
60+
- **Policy model**: Deny-by-default, flat grant list, first-match-wins. Designed for auditability.
61+
- **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`.
62+
- **Storage↔Audit decoupling**: `SecretStore` and `AuditLog` are independent — pick each backend separately. AWS SM provides only `SecretStore`; pair with `TracingAuditLog`.
63+
64+
### Feature Flags (core crate)
65+
66+
- `vsock` — enables tokio-vsock for VM communication (Linux only)
67+
- `kms` — enables AWS KMS key source
68+
69+
### Code Quality Principles
70+
71+
- Idiomatic Rust. Clippy clean. Experienced Rust developers should feel at home.
72+
- Prefer well-tested, well-established dependencies from known community members.
73+
- Tidy, efficient code. Well-named variables and functions. Readable by humans and agents.
74+
- Memory and CPU efficient — no needless clones. Compatible with the zeroclaw philosophy.
75+
- Use Rust types to prevent bugs (newtypes, enums, exhaustive matching).
76+
- A little macro-writing goes a long way for readability (`col!`, `parse_params!`, `json_response!`).

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = [".", "crates/zerolease-provider", "crates/zerolease-store-rusqlite", "crates/zerolease-store-aws-sm"]
2+
members = [".", "crates/zerolease-provider", "crates/zerolease-store-rusqlite", "crates/zerolease-store-aws-sm", "crates/zerolease-agent"]
33
# zerolease-store-postgres excluded from default workspace due to sqlx v0.8
44
# libsqlite3-sys conflict with rusqlite. Build/test it separately:
55
# cargo check -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml
@@ -58,6 +58,7 @@ chrono.workspace = true
5858
secrecy.workspace = true
5959
serde.workspace = true
6060
serde_json.workspace = true
61+
sha2 = "0.10"
6162
thiserror.workspace = true
6263
tokio.workspace = true
6364
tracing = "0.1"

README.md

Lines changed: 69 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,45 @@ When AI agents use tools that need credentials — API tokens, SSH keys, databas
1212

1313
## Design
1414

15-
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.
15+
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.
1616

1717
**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.
1818

1919
**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.
2020

2121
**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.
2222

23-
**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).
23+
**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.
2424

25-
**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.
25+
**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.
26+
27+
## Workspace
28+
29+
zerolease is a Cargo workspace. The core crate defines traits; storage and provider crates are chosen at compile time.
30+
31+
| Crate | Purpose |
32+
|-------|---------|
33+
| **zerolease** | Core: `Vault`, traits (`SecretStore`, `AuditLog`, `KeySource`), transports, policy engine |
34+
| **zerolease-store-rusqlite** | SQLite storage via rusqlite — `SecretStore` + `AuditLog` |
35+
| **zerolease-store-postgres** | PostgreSQL storage via sqlx — `SecretStore` + `AuditLog` |
36+
| **zerolease-store-aws-sm** | AWS Secrets Manager — `SecretStore` only (pair with `TracingAuditLog`) |
37+
| **zerolease-provider** | `CredentialProvider` trait for AI agent tool integration |
38+
39+
Pick a store crate and an audit backend independently:
40+
41+
- **Developer laptop:** rusqlite store + `RusqliteAuditLog` (single file, zero config)
42+
- **Cloud VMs:** AWS SM store + `TracingAuditLog` (logs to stdout → CloudWatch)
43+
- **Shared infra:** PostgreSQL store + `PostgresAuditLog`
44+
45+
## Transports
46+
47+
| Transport | Use case | Identity source |
48+
|-----------|----------|-----------------|
49+
| **Unix domain socket** | Developer machines, local processes | OS peer credentials (UID/PID) |
50+
| **TCP + token** | QEMU VMs via host-forwarded ports | Bearer token in `ClientHello` handshake |
51+
| **vsock** | Firecracker/QEMU via virtio | Guest CID (Linux only, feature `vsock`) |
52+
53+
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.
2654

2755
## Quick start
2856

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

3361
# Run the example (direct vault API, no server)
3462
cargo run --example basic_vault
35-
36-
# Or start the standalone server
37-
cargo run -- --socket /tmp/zerolease.sock --db secrets.db --audit-db audit.db
3863
```
3964

40-
The example stores a secret, requests a lease, accesses the credential through the lease, and demonstrates domain restriction.
41-
42-
## Feature flags
43-
44-
| Flag | Default | What it enables |
45-
| ---------- | ------- | ----------------------------------------------------- |
46-
| `sqlite` | Yes | SQLite secret store and audit log |
47-
| `postgres` | No | PostgreSQL secret store |
48-
| `kms` | No | AWS KMS envelope encryption key source |
49-
| `vsock` | No | vsock transport for Firecracker/QEMU VMs (Linux only) |
50-
5165
## Building
5266

5367
Requires Rust edition 2024.
5468

55-
```
56-
cargo build # default features (SQLite)
57-
cargo build --features postgres # with PostgreSQL support
58-
cargo build --features kms # with AWS KMS support
59-
cargo build --features vsock # with vsock transport (Linux only)
60-
cargo test # run tests (116 default)
61-
cargo doc --open # browse API documentation
69+
```bash
70+
cargo build # core + rusqlite store
71+
cargo test --workspace # run workspace tests
72+
cargo clippy --workspace # lint
73+
74+
# Excluded crate (built separately due to sqlx/rusqlite conflict):
75+
cargo build --manifest-path crates/zerolease-store-postgres/Cargo.toml
6276
```
6377

64-
## Testing
78+
### Feature flags (core crate)
6579

66-
The test suite covers unit tests, integration tests, security tests, and fuzz targets.
80+
| Flag | Default | What it enables |
81+
|---------|---------|-----------------|
82+
| `vsock` | No | vsock transport for Firecracker/QEMU VMs (Linux only) |
83+
| `kms` | No | AWS KMS envelope encryption key source |
84+
85+
## Testing
6786

6887
```bash
69-
# Default test suite (116 tests)
70-
cargo test
88+
# Workspace tests
89+
cargo test --workspace
7190

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

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

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

82-
# Fuzz targets (requires nightly)
83-
cargo +nightly fuzz run fuzz_read_frame -- -max_total_time=60
84-
cargo +nightly fuzz run fuzz_protocol_deser -- -max_total_time=60
85-
cargo +nightly fuzz run fuzz_domain_scope -- -max_total_time=60
102+
# OS keychain integration test
103+
cargo test -p zerolease keysource::keychain -- --ignored
86104
```
87105

88106
## Wire protocol
89107

90-
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`.
91-
92-
See the module documentation (`cargo doc --open`) for protocol details and type definitions.
108+
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`.
93109

94110
## Security
95111

96-
The vault has been through an adversarial security audit with all findings resolved:
97-
98-
- Secret material is zeroized on drop (`Zeroize`, `SecretString`, `Zeroizing<Vec<u8>>`)
99-
- Decryption errors are generic (no information leakage about failure cause)
112+
- Secret material zeroized on drop (`Zeroize`, `SecretString`, `Zeroizing<Vec<u8>>`)
113+
- Decryption errors are generic (no information leakage)
100114
- Domain scope matching rejects edge cases (empty subdomains, path traversal)
101115
- Policy engine is deny-by-default; empty prefix patterns are warned
102-
- Lease renewal is capped at 24 hours; per-agent lease count is capped
103-
- DEK rotation is atomic (database transaction)
104-
- SQL injection is prevented by parameterized queries (tested explicitly)
116+
- Lease renewal capped at 24 hours; per-agent lease count capped
117+
- SQL injection prevented by parameterized queries
105118
- Debug impls redact secret material
106119
- Role-based access control prevents agents from calling admin operations
107-
- Agent identity is bound at the transport level, not self-asserted
108-
- Wire protocol fuzz-tested (~3.5 million executions, zero crashes)
120+
- Agent identity bound at transport level, not self-asserted
121+
- Auth tokens stored as SHA-256 hashes, never in plaintext
122+
- TCP listener binds localhost only
123+
124+
## Status
125+
126+
Early development. Trait definitions and core types are stable. Concrete backend implementations are functional. Integration-level documentation and a standalone server binary are planned.
109127

110128
## License
111129

crates/zerolease-agent/Cargo.toml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[package]
2+
name = "zerolease-agent"
3+
version = "0.1.0"
4+
description = "VM-side agent: credential provisioner, lease-aware proxy, git credential helper"
5+
edition.workspace = true
6+
authors.workspace = true
7+
license.workspace = true
8+
repository.workspace = true
9+
10+
[[bin]]
11+
name = "zerolease-agent"
12+
path = "src/main.rs"
13+
14+
[dependencies]
15+
chrono.workspace = true
16+
clap = { version = "4", features = ["derive", "env"] }
17+
serde.workspace = true
18+
serde_json.workspace = true
19+
tokio = { workspace = true, features = ["full"] }
20+
tracing = "0.1"
21+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
22+
uuid.workspace = true
23+
zerolease = { workspace = true, default-features = false }
24+
25+
[dev-dependencies]
26+
tempfile = "3"
27+
28+
[target.'cfg(unix)'.dependencies]
29+
nix = { version = "0.29", features = ["process"] }
30+
31+
# Can't use workspace lints because the provisioner needs unsafe for env::set_var.
32+
[lints.rust]
33+
unsafe_code = "allow"
34+
future_incompatible = { level = "deny", priority = 1 }
35+
rust_2018_idioms = { level = "warn", priority = 2 }
36+
37+
[lints.clippy]
38+
unwrap_used = "deny"

0 commit comments

Comments
 (0)