Skip to content

Commit 892a34d

Browse files
authored
feat(security): M14 security hardening (#134)
* feat(security): add M14 security hardening Shell sandbox with configurable path restrictions and network control. Confirmation flows for destructive commands with CLI and Telegram support. A2A TLS enforcement, SSRF protection, configurable payload size limits. Structured audit logging with stdout/file destinations. Secret redaction in LLM responses with whitespace-preserving scanner. Configurable timeout policies for LLM, embedding, and A2A operations. Closes #91, closes #92, closes #93 * docs: update documentation for M14 security hardening Add changelog entries for shell sandbox, confirmation flows, A2A security, audit logging, secret redaction, and timeout policies. Update README with expanded Security section, new env vars, and config examples. Add security env vars to docker-compose files. Update setup-guide skill with new config keys and env overrides. * release: prepare v0.8.1
1 parent 2996c2e commit 892a34d

File tree

28 files changed

+1539
-149
lines changed

28 files changed

+1539
-149
lines changed

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
## [Unreleased]
88

9+
## [0.8.1] - 2026-02-10
10+
11+
### Added
12+
- Shell sandbox: configurable `allowed_paths` directory allowlist and `allow_network` toggle blocking curl/wget/nc in `ShellExecutor` (Issue #91)
13+
- Sandbox validation before every shell command execution with path canonicalization
14+
- `tools.shell.allowed_paths` config (empty = working directory only) with `ZEPH_TOOLS_SHELL_ALLOWED_PATHS` env override
15+
- `tools.shell.allow_network` config (default: true) with `ZEPH_TOOLS_SHELL_ALLOW_NETWORK` env override
16+
- Interactive confirmation for destructive commands (`rm`, `git push -f`, `DROP TABLE`, etc.) with CLI y/N prompt and Telegram inline keyboard (Issue #92)
17+
- `tools.shell.confirm_patterns` config with default destructive command patterns
18+
- `Channel::confirm()` trait method with default auto-confirm for headless/test scenarios
19+
- `ToolError::ConfirmationRequired` and `ToolError::SandboxViolation` variants
20+
- `execute_confirmed()` method on `ToolExecutor` for confirmation bypass after user approval
21+
- A2A TLS enforcement: reject HTTP endpoints when `a2a.require_tls = true` (Issue #92)
22+
- A2A SSRF protection: block private IP ranges (RFC 1918, loopback, link-local) with DNS resolution (Issue #92)
23+
- Configurable A2A server payload size limit via `a2a.max_body_size` (default: 1 MiB)
24+
- Structured JSON audit logging for all tool executions with stdout or file destination (Issue #93)
25+
- `AuditLogger` with `AuditEntry` (timestamp, tool, command, result, duration) and `AuditResult` enum
26+
- `[tools.audit]` config section with `ZEPH_TOOLS_AUDIT_ENABLED` and `ZEPH_TOOLS_AUDIT_DESTINATION` env overrides
27+
- Secret redaction in LLM responses: detect API keys, tokens, passwords, private keys and replace with `[REDACTED]` (Issue #93)
28+
- Whitespace-preserving `redact_secrets()` scanner with zero-allocation fast path via `Cow<str>`
29+
- `[security]` config section with `redact_secrets` toggle (default: true)
30+
- Configurable timeout policies for LLM, embedding, and A2A operations (Issue #93)
31+
- `[timeouts]` config section with `llm_seconds`, `embedding_seconds`, `a2a_seconds`
32+
- LLM calls wrapped with `tokio::time::timeout` in agent loop
33+
934
## [0.8.0] - 2026-02-10
1035

1136
### Added

Cargo.lock

Lines changed: 11 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ resolver = "3"
55
[workspace.package]
66
edition = "2024"
77
rust-version = "1.88"
8-
version = "0.8.0"
8+
version = "0.8.1"
99
authors = ["bug-ops"]
1010
license = "MIT"
1111
repository = "https://github.com/bug-ops/zeph"
@@ -51,14 +51,14 @@ tracing = "0.1"
5151
tracing-subscriber = "0.3"
5252
url = "2.5"
5353
uuid = "1.20"
54-
zeph-a2a = { path = "crates/zeph-a2a", version = "0.8.0" }
55-
zeph-channels = { path = "crates/zeph-channels", version = "0.8.0" }
56-
zeph-core = { path = "crates/zeph-core", version = "0.8.0" }
57-
zeph-llm = { path = "crates/zeph-llm", version = "0.8.0" }
58-
zeph-mcp = { path = "crates/zeph-mcp", version = "0.8.0" }
59-
zeph-memory = { path = "crates/zeph-memory", version = "0.8.0" }
60-
zeph-skills = { path = "crates/zeph-skills", version = "0.8.0" }
61-
zeph-tools = { path = "crates/zeph-tools", version = "0.8.0" }
54+
zeph-a2a = { path = "crates/zeph-a2a", version = "0.8.1" }
55+
zeph-channels = { path = "crates/zeph-channels", version = "0.8.1" }
56+
zeph-core = { path = "crates/zeph-core", version = "0.8.1" }
57+
zeph-llm = { path = "crates/zeph-llm", version = "0.8.1" }
58+
zeph-mcp = { path = "crates/zeph-mcp", version = "0.8.1" }
59+
zeph-memory = { path = "crates/zeph-memory", version = "0.8.1" }
60+
zeph-skills = { path = "crates/zeph-skills", version = "0.8.1" }
61+
zeph-tools = { path = "crates/zeph-tools", version = "0.8.1" }
6262

6363
[workspace.lints.clippy]
6464
all = "warn"

README.md

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ docker pull ghcr.io/bug-ops/zeph:latest
4848
Or use a specific version:
4949

5050
```bash
51-
docker pull ghcr.io/bug-ops/zeph:v0.8.0
51+
docker pull ghcr.io/bug-ops/zeph:v0.8.1
5252
```
5353

5454
**Security:** Images are scanned with [Trivy](https://trivy.dev/) in CI/CD and use Oracle Linux 9 Slim base with **0 HIGH/CRITICAL CVEs**. Multi-platform: linux/amd64, linux/arm64.
@@ -130,12 +130,28 @@ enabled = true
130130

131131
[tools.shell]
132132
timeout = 30
133-
blocked_commands = [] # Additional patterns beyond defaults
133+
blocked_commands = []
134+
allowed_commands = []
135+
allowed_paths = [] # Directories shell can access (empty = cwd only)
136+
allow_network = true # false blocks curl/wget/nc
137+
confirm_patterns = ["rm ", "git push -f", "git push --force", "drop table", "drop database", "truncate "]
134138

135139
[tools.scrape]
136140
timeout = 15
137141
max_body_bytes = 1048576 # 1MB
138142

143+
[tools.audit]
144+
enabled = false # Structured JSON audit log for tool executions
145+
destination = "stdout" # "stdout" or file path
146+
147+
[security]
148+
redact_secrets = true # Redact API keys/tokens in LLM responses
149+
150+
[timeouts]
151+
llm_seconds = 120 # LLM chat completion timeout
152+
embedding_seconds = 30 # Embedding generation timeout
153+
a2a_seconds = 30 # A2A remote call timeout
154+
139155
[vault]
140156
backend = "env" # "env" (default) or "age"
141157

@@ -151,7 +167,7 @@ rate_limit = 60
151167
</details>
152168

153169
> [!IMPORTANT]
154-
> Shell commands are filtered for safety. See [Security](#security) section for complete list of 12 blocked patterns and customization options.
170+
> Shell commands are sandboxed with path restrictions, network control, and destructive command confirmation. See [Security](#security) for details.
155171
156172
<details>
157173
<summary><b>🔧 Environment Variables</b> (click to expand)</summary>
@@ -178,6 +194,17 @@ rate_limit = 60
178194
| `ZEPH_A2A_PUBLIC_URL` | Public URL for agent card discovery |
179195
| `ZEPH_A2A_AUTH_TOKEN` | Bearer token for A2A server authentication |
180196
| `ZEPH_A2A_RATE_LIMIT` | Max requests per IP per minute (default: 60) |
197+
| `ZEPH_A2A_REQUIRE_TLS` | Require HTTPS for outbound A2A connections (default: true) |
198+
| `ZEPH_A2A_SSRF_PROTECTION` | Block private/loopback IPs in A2A client (default: true) |
199+
| `ZEPH_A2A_MAX_BODY_SIZE` | Max request body size in bytes (default: 1048576) |
200+
| `ZEPH_TOOLS_SHELL_ALLOWED_PATHS` | Comma-separated directories shell can access (empty = cwd) |
201+
| `ZEPH_TOOLS_SHELL_ALLOW_NETWORK` | Allow network commands from shell (default: true) |
202+
| `ZEPH_TOOLS_AUDIT_ENABLED` | Enable audit logging for tool executions (default: false) |
203+
| `ZEPH_TOOLS_AUDIT_DESTINATION` | Audit log destination: `stdout` or file path |
204+
| `ZEPH_SECURITY_REDACT_SECRETS` | Redact secrets in LLM responses (default: true) |
205+
| `ZEPH_TIMEOUT_LLM` | LLM call timeout in seconds (default: 120) |
206+
| `ZEPH_TIMEOUT_EMBEDDING` | Embedding generation timeout in seconds (default: 30) |
207+
| `ZEPH_TIMEOUT_A2A` | A2A remote call timeout in seconds (default: 30) |
181208

182209
</details>
183210

@@ -299,7 +326,7 @@ context_budget_tokens = 8000 # Set to LLM context window size (0 = unlimited)
299326

300327
## Docker
301328

302-
**Note:** Docker Compose automatically pulls the latest image from GitHub Container Registry. To use a specific version, set `ZEPH_IMAGE=ghcr.io/bug-ops/zeph:v0.8.0`.
329+
**Note:** Docker Compose automatically pulls the latest image from GitHub Container Registry. To use a specific version, set `ZEPH_IMAGE=ghcr.io/bug-ops/zeph:v0.8.1`.
303330

304331
<details>
305332
<summary><b>🐳 Docker Deployment Options</b> (click to expand)</summary>
@@ -359,7 +386,7 @@ ZEPH_VAULT_KEY=./my-key.txt ZEPH_VAULT_PATH=./my-secrets.age \
359386

360387
```bash
361388
# Use a specific release version
362-
ZEPH_IMAGE=ghcr.io/bug-ops/zeph:v0.8.0 docker compose up
389+
ZEPH_IMAGE=ghcr.io/bug-ops/zeph:v0.8.1 docker compose up
363390

364391
# Always pull latest
365392
docker compose pull && docker compose up
@@ -417,18 +444,75 @@ Zeph implements defense-in-depth security for safe AI agent operations in produc
417444
**Configuration:**
418445
```toml
419446
[tools.shell]
420-
timeout = 30 # Command execution timeout
447+
timeout = 30
421448
blocked_commands = ["custom_pattern"] # Additional patterns (additive to defaults)
449+
allowed_paths = ["/home/user/workspace"] # Restrict filesystem access
450+
allow_network = true # false blocks curl/wget/nc
451+
confirm_patterns = ["rm ", "git push -f"] # Destructive command patterns
422452
```
423453

424454
> [!IMPORTANT]
425-
> Custom patterns are **additive** — you cannot weaken default security. Matching is case-insensitive (`SUDO`, `Sudo`, `sudo` all blocked).
455+
> Custom blocked patterns are **additive** — you cannot weaken default security. Matching is case-insensitive.
456+
457+
### Shell Sandbox
458+
459+
Commands are validated against a configurable filesystem allowlist before execution:
460+
461+
- `allowed_paths = []` (default) restricts access to the working directory only
462+
- Paths are canonicalized to prevent traversal attacks (`../../etc/passwd`)
463+
- `allow_network = false` blocks network tools (`curl`, `wget`, `nc`, `ncat`, `netcat`)
464+
465+
### Destructive Command Confirmation
466+
467+
Commands matching `confirm_patterns` trigger an interactive confirmation before execution:
468+
469+
- **CLI:** `y/N` prompt on stdin
470+
- **Telegram:** inline keyboard with Confirm/Cancel buttons
471+
- Default patterns: `rm`, `git push -f`, `git push --force`, `drop table`, `drop database`, `truncate`
472+
- Configurable via `tools.shell.confirm_patterns` in TOML
473+
474+
### Audit Logging
475+
476+
Structured JSON audit log for all tool executions:
477+
478+
```toml
479+
[tools.audit]
480+
enabled = true
481+
destination = "./data/audit.jsonl" # or "stdout"
482+
```
483+
484+
Each entry includes timestamp, tool name, command, result (success/blocked/error/timeout), and duration in milliseconds.
485+
486+
### Secret Redaction
487+
488+
LLM responses are scanned for common secret patterns before display:
489+
490+
- Detected patterns: `sk-`, `AKIA`, `ghp_`, `gho_`, `xoxb-`, `xoxp-`, `sk_live_`, `sk_test_`, `-----BEGIN`
491+
- Secrets replaced with `[REDACTED]` preserving original whitespace formatting
492+
- Enabled by default (`security.redact_secrets = true`), applied to both streaming and non-streaming responses
493+
494+
### Timeout Policies
495+
496+
Configurable per-operation timeouts prevent hung connections:
497+
498+
```toml
499+
[timeouts]
500+
llm_seconds = 120 # LLM chat completion
501+
embedding_seconds = 30 # Embedding generation
502+
a2a_seconds = 30 # A2A remote calls
503+
```
504+
505+
### A2A Network Security
506+
507+
- **TLS enforcement:** `a2a.require_tls = true` rejects HTTP endpoints (HTTPS only)
508+
- **SSRF protection:** `a2a.ssrf_protection = true` blocks private IP ranges (RFC 1918, loopback, link-local) via DNS resolution
509+
- **Payload limits:** `a2a.max_body_size` caps request body (default: 1 MiB)
426510

427511
**Safe execution model:**
428-
- Commands parsed for blocked patterns before execution
512+
- Commands parsed for blocked patterns, then sandbox-validated, then confirmation-checked
429513
- Timeout enforcement (default: 30s, configurable)
430-
- Sandboxed execution with restricted environment
431514
- Full errors logged to system, sanitized messages shown to users
515+
- Audit trail for all tool executions (when enabled)
432516

433517
### Container Security
434518

config/default.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ port = 8080
106106
public_url = ""
107107
# Rate limit: max requests per minute per IP (0 = unlimited)
108108
rate_limit = 60
109+
# Require TLS for outbound A2A connections
110+
require_tls = true
111+
# Block requests to private/loopback IPs
112+
ssrf_protection = true
113+
# Maximum request body size in bytes (1MB)
114+
max_body_size = 1048576
109115

110116
[tools]
111117
# Enable tool execution (bash commands)
@@ -118,9 +124,33 @@ timeout = 30
118124
blocked_commands = []
119125
# Commands to remove from the default blocklist (e.g., ["curl", "wget"])
120126
allowed_commands = []
127+
# Restrict file access to these paths (empty = current directory only)
128+
allowed_paths = []
129+
# Allow network commands (curl, wget, nc)
130+
allow_network = true
131+
# Commands that require user confirmation before execution
132+
confirm_patterns = ["rm ", "git push -f", "git push --force", "drop table", "drop database", "truncate "]
121133

122134
[tools.scrape]
123135
# HTTP request timeout in seconds
124136
timeout = 15
125137
# Maximum response body size in bytes (1MB)
126138
max_body_bytes = 1048576
139+
140+
[tools.audit]
141+
# Enable audit logging for tool executions
142+
enabled = false
143+
# Audit destination: "stdout" or file path (e.g., "./data/audit.jsonl")
144+
destination = "stdout"
145+
146+
[security]
147+
# Redact secrets (API keys, tokens) from LLM responses before display
148+
redact_secrets = true
149+
150+
[timeouts]
151+
# LLM chat completion timeout in seconds
152+
llm_seconds = 120
153+
# Embedding generation timeout in seconds
154+
embedding_seconds = 30
155+
# A2A remote call timeout in seconds
156+
a2a_seconds = 30

crates/zeph-a2a/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ serde = { workspace = true, features = ["derive"] }
1919
subtle = { workspace = true, optional = true }
2020
serde_json.workspace = true
2121
thiserror.workspace = true
22-
tokio = { workspace = true, features = ["sync"] }
22+
tokio = { workspace = true, features = ["net", "sync"] }
23+
url.workspace = true
2324
tokio-stream.workspace = true
2425
tower = { workspace = true, optional = true }
2526
tower-http = { workspace = true, optional = true, features = ["limit", "trace"] }

0 commit comments

Comments
 (0)