diff --git a/README.md b/README.md index f089e7a..083f525 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Abbenay produces two packages from a single source tree: - **MCP aggregation**: Connect to external MCP servers, expose daemon as MCP server - **Dynamic model discovery**: Fetches available models from provider APIs - **Single Executable Application (SEA)**: Self-contained binary, no Node.js install required +- **Secure-by-default daemon**: HTTP on `127.0.0.1` with auth + CORS allowlist; gRPC TLS required off-loopback — see [SECURITY.md](docs/SECURITY.md) ## Why "Abbenay"? @@ -140,11 +141,14 @@ curl -H "Authorization: Bearer $ABBENAY_API_TOKEN" \ Works with Cursor, Continue, aider, and any `openai` SDK script (use the same token as the client API key). The HTTP server binds to `127.0.0.1` by default; -use `--host 0.0.0.0` only when you intentionally expose it. +use `--host 0.0.0.0` only when you intentionally expose it. gRPC TCP on +non-loopback addresses requires `--grpc-tls` (or explicit `--insecure`). > **WARNING:** Auth is on by default. `ABBENAY_HTTP_AUTH=0` disables it on any > bind (including `--host 0.0.0.0`) — for example a cluster-internal pod, or -> when a reverse proxy already authenticates callers. See +> when a reverse proxy already authenticates callers. Air-gap or offline use +> with local models reduces *egress*, but **network isolation alone does not +> secure Abbenay**. See [Security & air-gap](docs/SECURITY.md) and > [Configuration](docs/CONFIGURATION.md#http-api-security-server). ### Using the core library @@ -289,6 +293,7 @@ abbenay/ ## Documentation - [Getting Started](docs/GETTING_STARTED.md) +- [Security, Privacy & Air-Gap](docs/SECURITY.md) — defaults, residual risks, operator checklist - [Core Library (API Reference)](docs/CORE.md) - [Architecture](docs/ARCHITECTURE.md) - [Configuration](docs/CONFIGURATION.md) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4cab05a..d507704 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -110,7 +110,7 @@ The core TypeScript/Node.js process that runs as a background daemon. The web dashboard runs inside the daemon process via Express: -- **Port**: `localhost:8787` (configurable) +- **Bind / port**: `127.0.0.1:8787` by default (configurable; auth required) - **Static assets**: Served from `packages/daemon/static/` - **API routes**: `/api/*` -> Direct calls to `DaemonState` (no gRPC in the loop) - **Chat SSE**: `POST /api/chat` -> Streaming responses via Server-Sent Events @@ -491,7 +491,16 @@ internal MCP tool for cross-session retrieval. ## Security - **Secrets**: Stored in system keychain via keytar when available; never in config files -- **Socket**: Unix socket (or named pipe) with user-only permissions -- **Web dashboard**: Listens on localhost only -- **No remote access**: Daemon designed for local use only +- **Socket**: Unix socket (or named pipe) with user-only permissions (local IPC) +- **Web dashboard / HTTP API**: Binds to `127.0.0.1` by default; Bearer (or + cookie + CSRF) auth required; CORS allowlist only (never `*`) — DR-030 +- **gRPC TCP**: Loopback plaintext OK for local DX; non-loopback requires + `--grpc-tls` or explicit `--insecure` — DR-029. Empty `consumers` refused + off-loopback unless `--allow-open-auth` / `--insecure` — DR-037 +- **Air-gap / privacy**: Local and on-prem engines can keep prompts off the + public internet, but **network isolation alone does not secure the daemon**. + See [SECURITY.md](./SECURITY.md). - **Config files**: Created with mode `0o600` (user read/write only) + +Intentional exposure (containers, LAN) is opt-in via `--host` / image CMD — +see [CONTAINER.md](./CONTAINER.md) and [SECURITY.md](./SECURITY.md). diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index a20083c..d325c84 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -89,6 +89,10 @@ MCP endpoint (`/mcp`) require authentication by default. MCP tool calls also honor `tool_policy` (same approval path as chat — see [Tool policy](#tool-policy) below). +For air-gap / privacy claims vs real defaults, see +[SECURITY.md](./SECURITY.md). **Network isolation alone does not secure +Abbenay.** + | Setting / env | Purpose | Default | |---------------|---------|---------| | `ABBENAY_API_TOKEN` or `server.api_token` / `server.api_token_env` | Bearer token for all HTTP routes | Auto-generated and stored as `http-api-token` in the config directory | diff --git a/docs/CONTAINER.md b/docs/CONTAINER.md index d43c975..0235802 100644 --- a/docs/CONTAINER.md +++ b/docs/CONTAINER.md @@ -356,6 +356,11 @@ spec: ## Security: HTTP bind and authentication +> Workstation CLI defaults bind HTTP to `127.0.0.1` with auth on. The +> **container image CMD** intentionally uses `--host 0.0.0.0` so published +> ports work — that is opt-in exposure, not “air-gap security.” See +> [SECURITY.md](./SECURITY.md). + | Flag | Default | Effect | |------|---------|--------| | `--host` / `ABBENAY_HTTP_HOST` / `server.host` | `127.0.0.1` | HTTP bind (dashboard, `/api/*`, `/v1/*`, `/mcp`) | diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 6ff7bf5..caa87b0 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -257,7 +257,9 @@ same token as the API key. > logs a warning. Use auth-off when something else already protects the API > (for example a production pod on a private cluster network, or a reverse > proxy / gateway that authenticates callers). Otherwise keep auth on and -> use `ABBENAY_API_TOKEN`. +> use `ABBENAY_API_TOKEN`. Defaults bind HTTP to `127.0.0.1` with CORS +> allowlisted — see [SECURITY.md](SECURITY.md). Offline / air-gap use with +> local models does **not** replace these controls. ### With the OpenAI Python SDK @@ -394,6 +396,7 @@ aby list-engines ## Next steps +- [Security, Privacy & Air-Gap](SECURITY.md) — defaults vs air-gap claims - [Configuration Reference](CONFIGURATION.md) — all config options - [Core Library API](CORE.md) — use `@abbenay/core` in your own apps - [Architecture](ARCHITECTURE.md) — how the system fits together diff --git a/docs/LANDSCAPE.md b/docs/LANDSCAPE.md index 2cadda1..564ab96 100644 --- a/docs/LANDSCAPE.md +++ b/docs/LANDSCAPE.md @@ -78,10 +78,14 @@ aggregation, and a web dashboard. **Strengths:** no infrastructure to deploy, works offline with local models, secrets stay on the developer's machine, same core powers CLI / daemon / extension / web, in-process tool execution with approval -policies. +policies. Secure-by-default HTTP (localhost bind, auth, CORS allowlist) +and fail-closed gRPC TLS off-loopback — see [SECURITY.md](./SECURITY.md). **Trade-offs:** not designed for team-wide centralized control, no -built-in cost tracking or billing, earlier in development. +built-in cost tracking or billing, earlier in development. Offline / +air-gap routing to local models reduces public-cloud egress; it does +**not** by itself authenticate callers or replace daemon security +controls. --- diff --git a/docs/PRODUCT_OVERVIEW.md b/docs/PRODUCT_OVERVIEW.md index 66d3517..cda6c0d 100644 --- a/docs/PRODUCT_OVERVIEW.md +++ b/docs/PRODUCT_OVERVIEW.md @@ -2,7 +2,7 @@ **Status:** MVP Complete **Version:** 0.0.0-dev -**Last Updated:** March 2026 +**Last Updated:** July 2026 --- @@ -252,6 +252,19 @@ providers: --- +## Security and air-gap posture + +Abbenay supports **local and on-prem engines** (Ollama, Red Hat AI, LM Studio, +custom OpenAI-compatible endpoints) so operators can keep prompts inside their +network. That is a routing choice — not a security boundary by itself. + +Defaults: HTTP on `127.0.0.1` with auth and CORS allowlist; gRPC TLS required +off-loopback. Abbenay does **not** ship network-isolation controls (firewall / +offline enforcement); operators own that layer. **Network isolation alone does +not secure Abbenay.** See [SECURITY.md](./SECURITY.md). + +--- + ## Out of Scope | Item | Notes | diff --git a/docs/REDHAT_AI.md b/docs/REDHAT_AI.md index 047f0d2..fa47be1 100644 --- a/docs/REDHAT_AI.md +++ b/docs/REDHAT_AI.md @@ -13,6 +13,10 @@ an OpenAI-compatible REST API: Both speak the same protocol, so Abbenay routes them through `@ai-sdk/openai-compatible` — no additional SDK packages are required. +Local Inference Server deployments support air-gapped / offline inference +(prompts stay on your network). That does **not** mean network isolation +secures the daemon — see [SECURITY.md](./SECURITY.md). + --- ## Profile A — Inference Server @@ -189,9 +193,10 @@ providers: | 5 | Model discovery (CLI) | `aby list-models --discover redhat` | | 6 | Model discovery (web) | Add Provider wizard → select `redhat` | | 7 | Chat completions | `aby chat -m redhat-inference/` | -| 8 | Streaming | `curl http://localhost:8787/v1/chat/completions` with `"stream": true` | +| 8 | Streaming | `curl -H "Authorization: Bearer $ABBENAY_API_TOKEN" http://127.0.0.1:8787/v1/chat/completions` with `"stream": true` | | 9 | Tool calling | `aby chat` with tools enabled (if model supports it) | | 10 | VS Code LM API | Configure Provider → select model → chat | +| 11 | Air-gap / privacy | Use local Inference Server only; isolation alone does not secure Abbenay — see [SECURITY.md](./SECURITY.md) | --- @@ -202,3 +207,4 @@ providers: - [Red Hat AI Inference — Configuring API key authentication](https://docs.redhat.com/en/documentation/red_hat_ai_inference/3.4/html/getting_started/configuring-api-key-authentication_getting-started) - [RHEL AI 3.4 — Getting Started](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux_ai/3.4/html-single/getting_started/getting_started) - [Abbenay Configuration Guide](CONFIGURATION.md) +- [Security, Privacy & Air-Gap](SECURITY.md) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 2f365d5..a0fee22 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -122,7 +122,10 @@ aider, any `openai` SDK script, etc.). See DR-020. | `POST /v1/completions` | low — legacy | | `POST /v1/embeddings` | low — if engines support it | | Usage/token stats (real counts from providers) | medium | -| Bearer token auth on all HTTP routes (`server.api_token` / `ABBENAY_API_TOKEN`) | **done** | +| Bearer token auth on all HTTP routes (`server.api_token` / `ABBENAY_API_TOKEN`) | **done** (DR-030) | +| Localhost-first HTTP bind + CORS allowlist (never `*`) | **done** (DR-030) | +| gRPC TLS / fail-closed non-loopback binds | **done** (DR-029) | +| Air-gap / privacy docs aligned with defaults (finding A4) | **done** — [SECURITY.md](./SECURITY.md) | | Rate limiting | low | ### Key files @@ -260,6 +263,18 @@ by wiring to `ToolRegistry`. --- +## 8. Air-gap docs vs network exposure (A4) — done + +Product copy historically overstated air-gap / privacy while defaults exposed +HTTP on all interfaces, plaintext gRPC, and wildcard CORS. Docs now state the +real defaults and that **network isolation alone does not secure Abbenay** — +see [SECURITY.md](./SECURITY.md) (DR-038), including the operator checklist +(bind, auth, CORS, TLS, consumers, MCP) and residual risks. Release notes: +`packages/vscode/CHANGELOG.md` (Unreleased → Security). Code fixes: DR-030 / +DR-029. + +--- + ## Suggested implementation order | Phase | Sections | Rationale | Status | diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..473d3e1 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,194 @@ +# Security, Privacy, and Air-Gap + +Abbenay can use **local / on-prem models** (Ollama, Red Hat AI Inference +Server, LM Studio, and similar) so prompts need not leave your network. +That is a **data-routing** property — not authentication, authorization, or +TLS. + +> **Network isolation alone does not secure Abbenay.** +> An air-gapped or offline host can still expose a reachable daemon to every +> local process and every client that can contact the bind address. Do not +> treat “no internet” as a substitute for the defaults below. + +Code fixes for the historical gaps (all-interface HTTP, wildcard CORS, +plaintext gRPC off-loopback) live in DR-030 / AAP-82788 and DR-029 / +AAP-82804. This page aligns product claims with that posture (finding A4). + +Config details: [CONFIGURATION.md](./CONFIGURATION.md). Containers: +[CONTAINER.md](./CONTAINER.md). Verify a live deploy with the +[operator checklist](#operator-checklist) below. + +--- + +## Default posture (after secure defaults) + +| Control | Default | +|---------|---------| +| **HTTP bind** | `127.0.0.1` (not `0.0.0.0`) | +| **HTTP auth** | Required (Bearer / dashboard session) | +| **CORS** | Explicit allowlist — never `*` | +| **gRPC TCP** | Loopback plaintext OK; non-loopback needs `--grpc-tls` or `--insecure` | + +### What this addresses from finding A4 + +| Historical issue | Current posture | +|------------------|-----------------| +| HTTP on `0.0.0.0` (H1) | Binds `127.0.0.1` | +| Wildcard CORS (C1) | Allowlist only; auth required | +| Plaintext gRPC when exposed (C2) | TLS (or explicit `--insecure`) off-loopback | +| No network isolation controls | **Not a product feature** — see below | + +### No network isolation controls (by design) + +Abbenay does **not** provide firewall, VLAN, offline-enforcement, or other +network-isolation features. Air-gap / offline networking is an **operator / +environment** concern (host firewall, air-gapped NIC, no default route, etc.). + +What Abbenay *does* provide are daemon exposure controls: + +| Control | Role | +|---------|------| +| **Bind address** | Limits which interfaces accept connections | +| **HTTP auth** | Blocks anonymous API / dashboard / MCP access | +| **CORS allowlist** | Stops arbitrary websites from calling the daemon in a browser | +| **gRPC TLS / `--insecure`** | Protects or explicitly opts out of encryption off-loopback | +| **Consumers** (gRPC) | Capability tokens for non-loopback gRPC clients | + +Documenting these is how A4 is closed — not by inventing an isolation product. +Operators who need isolation must apply it outside Abbenay **and** keep these +defaults (or stronger) in place. + +### Residual risks even when air-gapped + +- Same-user local processes can still reach loopback / Unix sockets. +- Stolen API or consumer tokens still work. +- `--host 0.0.0.0`, `ABBENAY_HTTP_AUTH=0`, and `--insecure` deliberately weaken posture. +- Cloud providers still receive prompts if you configure them. +- HTTP on loopback remains plaintext; use a TLS-terminating proxy when exposing beyond the machine. + +--- + +## Operator checklist + +Run these after a fresh default start (`aby web` or `abbenay daemon` with no +host/TLS override). Replace `8787` if you use another HTTP port. Load the API +token from `ABBENAY_API_TOKEN` or the auto-generated `http-api-token` file in +the config directory (`~/Library/Application Support/abbenay` on macOS, +`~/.config/abbenay` on Linux, `%APPDATA%\abbenay` on Windows). + +```bash +export ABBENAY_API_TOKEN="${ABBENAY_API_TOKEN:-$(cat "$HOME/Library/Application Support/abbenay/http-api-token" 2>/dev/null || cat "$HOME/.config/abbenay/http-api-token")}" +``` + +### 1. Bind address + +Confirm HTTP listens on loopback only (not `0.0.0.0`): + +```bash +# macOS / BSD +lsof -nP -iTCP:8787 -sTCP:LISTEN +# Linux +ss -ltnp 'sport = :8787' +``` + +**Pass:** listener address is `127.0.0.1:8787` (or `[::1]:8787`). +**Fail:** `*:8787`, `0.0.0.0:8787`, or a LAN IP without an intentional `--host` opt-in. + +### 2. HTTP auth + +```bash +# Unauthenticated API → 401 +curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8787/api/health +# Authenticated → 200 +curl -s -o /dev/null -w '%{http_code}\n' \ + -H "Authorization: Bearer $ABBENAY_API_TOKEN" \ + http://127.0.0.1:8787/api/health +``` + +**Pass:** first prints `401`, second prints `200`. +**Fail:** unauthenticated request succeeds, or auth cannot be satisfied with the token. + +### 3. CORS allowlist + +```bash +curl -s -o /dev/null -w '%{http_code}\n' \ + -H 'Origin: https://evil.example' \ + -H "Authorization: Bearer $ABBENAY_API_TOKEN" \ + http://127.0.0.1:8787/api/health + +curl -s -D - -o /dev/null \ + -H 'Origin: http://127.0.0.1:8787' \ + -H "Authorization: Bearer $ABBENAY_API_TOKEN" \ + http://127.0.0.1:8787/api/health | grep -i access-control-allow-origin +``` + +**Pass:** foreign Origin returns `403` and no `Access-Control-Allow-Origin: *`; +localhost Origin is echoed in `Access-Control-Allow-Origin`. +**Fail:** response allows `*` or accepts an arbitrary website Origin. + +### 4. gRPC TLS (exposure path) + +Default daemon has no TCP gRPC listener. When you enable TCP: + +```bash +# Loopback plaintext OK +abbenay daemon --grpc-port 50051 +# Must fail without TLS or --insecure +abbenay daemon --grpc-port 50051 --grpc-host 0.0.0.0 +# Must succeed only with explicit opt-in (+ consumers; see step 5) +abbenay daemon --grpc-port 50051 --grpc-host 0.0.0.0 --grpc-tls +``` + +**Pass:** non-loopback without `--grpc-tls` / `--insecure` refuses to start; +with `--grpc-tls`, logs show TLS (see [DEVELOPMENT.md](./DEVELOPMENT.md)). +**Fail:** plaintext gRPC accepts connections on `0.0.0.0` without `--insecure`. + +### 5. Consumers (gRPC) + +On non-loopback TCP, an empty/missing `consumers` section must refuse to start +unless `--allow-open-auth` or `--insecure` is set. Configure consumers per +[CONFIGURATION.md](./CONFIGURATION.md#consumer-authentication-consumers), then: + +```bash +# After consumers + --grpc-tls on 0.0.0.0: sensitive RPCs need x-abbenay-token +# Wrong/missing token → PERMISSION_DENIED (see consumer-auth tests / client docs) +``` + +**Pass:** start fails with empty consumers on `0.0.0.0`; with consumers, wrong +token is denied on gated RPCs. +**Fail:** non-loopback gRPC allows all callers with no consumers and no opt-in. + +### 6. MCP HTTP endpoint + +```bash +# Unauthenticated /mcp → 401 +curl -s -o /dev/null -w '%{http_code}\n' \ + -X POST http://127.0.0.1:8787/mcp \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' +``` + +**Pass:** `401` without Bearer. With Bearer, connection consent is still +required before a session is issued (`GET/POST /api/mcp/connections` — see +[CONFIGURATION.md](./CONFIGURATION.md#mcp-client-connection-consent)). +**Fail:** anonymous clients can open `/mcp` or skip consent. + +### Operator controls outside Abbenay + +These are **not** product features; confirm them in your environment if you +need air-gap / offline posture: + +- Host firewall / security groups restrict who can reach any non-loopback bind +- No unintended default route / NIC bridging for offline hosts +- Strong tokens set via env (`ABBENAY_API_TOKEN`, consumer `token_env`) in + containers — do not rely on auto-generated files alone + +--- + +## Decision references + +| Decision | Topic | +|----------|--------| +| DR-029 | Fail-closed TLS for non-loopback gRPC TCP | +| DR-030 | Secure-by-default HTTP (auth, CORS, bind) | +| DR-038 | Air-gap docs must not claim isolation equals security | diff --git a/docs/decisions.md b/docs/decisions.md index 231fcf0..6bd6585 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -612,6 +612,27 @@ compare closes token oracle leaks. --- +## DR-038: Air-gap docs must match secure defaults (finding A4) + +**Date:** 2026-07-20 +**Decision:** After DR-029 / DR-030 land, product and feature documentation +MUST describe the real localhost-first HTTP defaults, auth, CORS allowlist, +and gRPC TLS posture, and MUST NOT claim that network isolation or air-gap +alone secures Abbenay. Docs MUST also state that Abbenay does **not** provide +network-isolation features (firewall / offline enforcement); isolation remains +an operator/environment control, while bind/auth/CORS/TLS/consumers are the +daemon controls. Residual risks, a step-by-step operator checklist (bind, +auth, CORS, TLS, consumers, MCP), and changelog callouts for the security +default changes are required in `docs/SECURITY.md` and +`packages/vscode/CHANGELOG.md`. +**Rationale:** Finding A4 — messaging emphasized air-gap / privacy while +historical defaults (all-interface HTTP, plaintext gRPC, wildcard CORS) and +the absence of product-level isolation controls could leave an admin believing +isolation equals security. Technical fixes live in AAP-82788 / AAP-82804; this +decision closes the docs / claims half. + +--- + ## DR-039: Block MCP self-connections to the daemon's own endpoints **Date:** 2026-07-20 diff --git a/packages/vscode/CHANGELOG.md b/packages/vscode/CHANGELOG.md index 6c08dae..3feeb00 100644 --- a/packages/vscode/CHANGELOG.md +++ b/packages/vscode/CHANGELOG.md @@ -8,6 +8,19 @@ This project uses [CalVer](https://calver.org/) versioning (`YYYY.M.MICRO`). ## [Unreleased] +### Security + +- **Secure-by-default HTTP (AAP-82788 / DR-030):** bind defaults to `127.0.0.1` + (not `0.0.0.0`); Bearer / dashboard auth required on `/api/*`, `/v1/*`, and + `/mcp`; CORS is an explicit allowlist (never `*`). +- **Fail-closed gRPC TCP (AAP-82804 / DR-029):** non-loopback binds require + `--grpc-tls` or explicit `--insecure`; empty `consumers` refused off-loopback + unless `--allow-open-auth` / `--insecure`. +- **Air-gap docs (AAP-82838 / A4 / DR-038):** product copy no longer implies + network isolation alone secures Abbenay. See + [SECURITY.md](../../docs/SECURITY.md) for defaults, residual risks, and the + operator checklist (bind, auth, CORS, TLS, consumers, MCP). + ### Added - Initial VS Code Marketplace release diff --git a/packages/vscode/README.md b/packages/vscode/README.md index ea1aba5..7234346 100644 --- a/packages/vscode/README.md +++ b/packages/vscode/README.md @@ -87,11 +87,15 @@ aby web # Start web dashboard aby start # Or start everything at once ``` -Open http://localhost:8787 to: +Open http://127.0.0.1:8787 to: - Add API keys (stored in system keychain or referenced from environment variables) - Enable/disable providers - Select which models to expose +HTTP auth is on by default — use the dashboard login / API token when prompted. +The daemon binds to loopback by default; see +[Security & air-gap](../../docs/SECURITY.md). + ### Config Files Configuration is stored in YAML files: @@ -226,7 +230,7 @@ npm run package ### Models Not Appearing -1. Open web dashboard (http://localhost:8787) +1. Open web dashboard (http://127.0.0.1:8787) — sign in with the API token if prompted 2. Ensure provider has valid API key 3. Enable the models you want 4. Reload VS Code window