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
4 changes: 2 additions & 2 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ USER ${APP_USER}
EXPOSE 8787 50051

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -sf http://localhost:8787/api/health || exit 1
CMD curl -sf -H "Authorization: Bearer ${ABBENAY_API_TOKEN}" http://127.0.0.1:8787/api/health || exit 1

ENTRYPOINT ["./abbenay"]
CMD ["start", "--port", "8787", "--grpc-port", "50051", "--grpc-host", "0.0.0.0", "--grpc-tls"]
CMD ["start", "--port", "8787", "--host", "0.0.0.0", "--grpc-port", "50051", "--grpc-host", "0.0.0.0", "--grpc-tls"]
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,21 @@ Any tool that speaks the OpenAI protocol can use Abbenay as a backend:
```bash
aby serve -p 8787

# Then point your client at it:
curl http://localhost:8787/v1/models
curl http://localhost:8787/v1/chat/completions \
# HTTP routes require a Bearer token (ABBENAY_API_TOKEN or auto-generated http-api-token):
curl -H "Authorization: Bearer $ABBENAY_API_TOKEN" http://127.0.0.1:8787/v1/models
curl -H "Authorization: Bearer $ABBENAY_API_TOKEN" \
http://127.0.0.1:8787/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "openai/gpt-4o", "messages": [{"role": "user", "content": "Hello"}]}'
```

Works with Cursor, Continue, aider, and any `openai` SDK script.
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.

> **WARNING:** Auth is on by default. `ABBENAY_HTTP_AUTH=0` disables it for
> local development only — do not use with a non-loopback bind. See
> [Configuration](docs/CONFIGURATION.md#http-api-security-server).

### Using the core library

Expand Down
60 changes: 60 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ Abbenay uses YAML configuration files and system keychain for secrets.
## Config File Format

```yaml
# HTTP API security (optional overrides — secure defaults apply without this block)
server:
api_token_env: "ABBENAY_API_TOKEN" # preferred: token from env (never commit secrets)
# api_token: "..." # discouraged plaintext; prefer api_token_env
host: "127.0.0.1" # default bind; use 0.0.0.0 only intentionally
cors_origins: # extra allowed Origins (localhost always included)
- "https://my-trusted-app.example"

providers:
my-openai: # Virtual provider name (user-defined)
engine: openai # Engine type (see Supported Engines below)
Expand Down Expand Up @@ -40,6 +48,58 @@ providers:
model_id: "qwen2.5-coder:7b" # Map virtual name to actual model ID
```

### HTTP API security (`server`)

The web dashboard, REST API (`/api/*`), OpenAI-compatible API (`/v1/*`), and
MCP endpoint (`/mcp`) require authentication by default.

| 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 |
| `ABBENAY_HTTP_AUTH` | Enable/disable HTTP auth | Enabled (`1` / unset). Set to `0`, `false`, `off`, `no`, or `disabled` to turn auth off |
| `ABBENAY_HTTP_HOST` or `server.host` or `--host` | HTTP bind address | `127.0.0.1` |
| `ABBENAY_CORS_ORIGINS` or `server.cors_origins` | Extra CORS allowed origins | `http://127.0.0.1:<port>`, `http://localhost:<port>` |

Call APIs with:

```bash
curl -H "Authorization: Bearer $ABBENAY_API_TOKEN" http://127.0.0.1:8787/api/health
```

The dashboard uses SameSite=Strict cookies plus a CSRF token for browser
session auth. Open `http://127.0.0.1:8787/login` (or `POST /login` with the
token in the body) to establish a session — prefer that over putting the
token in the URL. Cookies include the `Secure` flag when the request is
HTTPS or arrives via a TLS-terminated proxy (`X-Forwarded-Proto: https`).
Binding to `0.0.0.0` requires an explicit opt-in and logs a warning — only
do this when you intentionally expose the HTTP API (e.g. containers) and
have set a strong token.

> **WARNING — disabling HTTP auth:** Auth is **on by default**. For throwaway
> local development only you may set `ABBENAY_HTTP_AUTH=0`. That allows any
> process (and any website that can reach the bind address) to call the
> daemon and read/write secrets, config, chat, MCP, and sessions. The server
> logs a loud warning when auth is disabled. Combining `ABBENAY_HTTP_AUTH=0`
> with `--host 0.0.0.0` (or any non-loopback bind) fails closed — the HTTP
> server refuses to start. Prefer keeping auth enabled and using a local
> token instead.

### Session ownership

Every session is stamped with an `owner` principal:

| Surface | Owner |
|---------|--------|
| CLI (`aby chat` / `aby sessions`) | `local` |
| HTTP API (Bearer / dashboard cookie) | `http:<token-fingerprint>` |
| HTTP + `X-Abbenay-Session-Owner: <name>` | `http:<fingerprint>:<name>` |
| gRPC with matching consumer token | `consumer:<name>` |
| gRPC without consumer token | `local` |

List/get/delete/chat only return sessions for the caller's owner. Cross-owner
access returns 404 (not 403) so session IDs are not leaked across principals.
Legacy sessions without an `owner` field are treated as `local`.

### Key Concepts

- **Virtual provider name** - The YAML key (e.g., `my-openai`). User-defined, must be lowercase alphanumeric with dots, hyphens, or underscores.
Expand Down
57 changes: 51 additions & 6 deletions docs/CONTAINER.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Mount this file into the container at:
podman run -d --name abbenay \
-v ./config.yaml:/home/abbenay/.config/abbenay/config.yaml:ro \
-e OPENROUTER_API_KEY=sk-or-... \
-e ABBENAY_API_TOKEN=change-me \
-p 8787:8787 \
-p 50051:50051 \
abbenay:latest
Expand All @@ -110,6 +111,7 @@ With consumer authentication (for programmatic clients like APME):
podman run -d --name abbenay \
-v ./config.yaml:/home/abbenay/.config/abbenay/config.yaml:ro \
-e OPENROUTER_API_KEY=sk-or-... \
-e ABBENAY_API_TOKEN=change-me \
-e APME_TOKEN=secret123 \
-p 8787:8787 \
-p 50051:50051 \
Expand All @@ -119,9 +121,12 @@ podman run -d --name abbenay \
### Verify it's running

```bash
curl http://localhost:8787/api/health
curl -H "Authorization: Bearer $ABBENAY_API_TOKEN" http://127.0.0.1:8787/api/health
```

Set `ABBENAY_API_TOKEN` in the container environment (required for the
built-in healthcheck and all HTTP routes).

### View logs

```bash
Expand All @@ -133,8 +138,8 @@ podman logs -f abbenay
## Overriding the command

The default `CMD` is
`start --port 8787 --grpc-port 50051 --grpc-host 0.0.0.0 --grpc-tls`,
which runs all services with TLS-protected gRPC accessible from outside the
`start --port 8787 --host 0.0.0.0 --grpc-port 50051 --grpc-host 0.0.0.0 --grpc-tls`,
which runs all services with HTTP and TLS-protected gRPC accessible from outside the
container. You can override it to run a subset:

```bash
Expand Down Expand Up @@ -243,6 +248,9 @@ metadata:
type: Opaque
stringData:
OPENROUTER_API_KEY: "sk-or-..."
# Must match the Bearer value in the probe httpHeaders below.
# Kubernetes does not expand env vars in httpGet.httpHeaders.
ABBENAY_API_TOKEN: "replace-with-a-strong-token"
---
apiVersion: v1
kind: ConfigMap
Expand Down Expand Up @@ -291,12 +299,18 @@ spec:
httpGet:
path: /api/health
port: 8787
httpHeaders:
- name: Authorization
value: Bearer replace-with-a-strong-token
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/health
port: 8787
httpHeaders:
- name: Authorization
value: Bearer replace-with-a-strong-token
initialDelaySeconds: 5
periodSeconds: 10
volumes:
Expand Down Expand Up @@ -324,14 +338,45 @@ spec:

- The image runs as non-root user `abbenay` (UID 1001), compatible with
OpenShift's restricted SCC.
- The built-in `HEALTHCHECK` uses `curl` against `/api/health`. In
Kubernetes, use the `livenessProbe` and `readinessProbe` shown above
instead.
- The built-in `HEALTHCHECK` uses `curl` against `/api/health` with
`Authorization: Bearer ${ABBENAY_API_TOKEN}`. Set that env var when
running the container. In Kubernetes, the sample `livenessProbe` /
`readinessProbe` send the same Bearer token via `httpHeaders` — keep that
value identical to `ABBENAY_API_TOKEN` in the Secret (Kubernetes does not
expand environment variables in `httpGet.httpHeaders`).
- Sessions are ephemeral by default. To persist sessions across restarts,
mount a volume at `/home/abbenay/.local/share/abbenay/sessions/`.

---

## Security: HTTP bind and authentication

| Flag | Default | Effect |
|------|---------|--------|
| `--host` / `ABBENAY_HTTP_HOST` / `server.host` | `127.0.0.1` | HTTP bind (dashboard, `/api/*`, `/v1/*`, `/mcp`) |

| Value | Effect |
|-------|--------|
| `127.0.0.1` (default) | Loopback only — safe for local development |
| `0.0.0.0` | All interfaces — required inside containers so published ports are reachable |

The container's default `CMD` uses `--host 0.0.0.0` because container
networking requires listeners to accept connections from outside the
container's network namespace. The daemon logs a warning when HTTP is bound
beyond loopback.

**HTTP authentication is on by default.** Set `ABBENAY_API_TOKEN` (or
`server.api_token` / `server.api_token_env`) and pass
`Authorization: Bearer <token>` on every request. CORS is allowlist-only
(never `*`).

> **WARNING:** `ABBENAY_HTTP_AUTH=0` disables HTTP auth for local development
> only. Combining it with `--host 0.0.0.0` (or any non-loopback bind) fails
> closed — the HTTP server refuses to start.

When exposing HTTP, always set a strong `ABBENAY_API_TOKEN` and restrict
`server.cors_origins`. Keep `ABBENAY_HTTP_AUTH` enabled (the default).

## Security: gRPC bind, TLS, and `--insecure`

The `--grpc-host` flag controls which network interface the TCP gRPC
Expand Down
37 changes: 30 additions & 7 deletions docs/GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,28 +230,43 @@ Start the server:
aby serve -p 8787
```

Then point any OpenAI-compatible client at it:
HTTP routes require a Bearer token (`ABBENAY_API_TOKEN`, `server.api_token`, or
the auto-generated `http-api-token` in your config directory):

```bash
# List models
curl http://localhost:8787/v1/models
curl -H "Authorization: Bearer $ABBENAY_API_TOKEN" \
http://127.0.0.1:8787/v1/models

# Chat (streaming)
curl http://localhost:8787/v1/chat/completions \
curl -H "Authorization: Bearer $ABBENAY_API_TOKEN" \
-H "Content-Type: application/json" \
http://127.0.0.1:8787/v1/chat/completions \
-d '{
"model": "my-openai/gpt-4o",
"messages": [{"role": "user", "content": "Hello!"}],
"stream": true
}'
```

Point any OpenAI-compatible client at `http://127.0.0.1:8787/v1` and use the
same token as the API key.

> **WARNING:** HTTP auth is **enabled by default**. For throwaway local
> development only you may set `ABBENAY_HTTP_AUTH=0` to skip Bearer tokens.
> The server logs a warning. Combining that with `--host 0.0.0.0` refuses to
> start. Prefer keeping auth on and using `ABBENAY_API_TOKEN` instead.

### With the OpenAI Python SDK

```python
from openai import OpenAI
import os

client = OpenAI(base_url="http://localhost:8787/v1", api_key="unused")
client = OpenAI(
base_url="http://127.0.0.1:8787/v1",
api_key=os.environ["ABBENAY_API_TOKEN"],
)
response = client.chat.completions.create(
model="my-openai/gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
Expand All @@ -261,8 +276,8 @@ print(response.choices[0].message.content)

### With Cursor / Continue / aider

Set the API base URL to `http://localhost:8787/v1` in your tool's
settings. The API key field can be any non-empty string.
Set the API base URL to `http://127.0.0.1:8787/v1` in your tool's
settings. Use your Abbenay HTTP API token as the API key.

---

Expand All @@ -272,13 +287,21 @@ settings. The API key field can be any non-empty string.
aby web
```

Open http://localhost:8787 in your browser to:
Open http://127.0.0.1:8787 in your browser (loopback clients get a session
automatically). For remote binds, use http://127.0.0.1:8787/login or
`POST /login` with the API token in the body — avoid putting the token in
the query string (it can leak via history, Referer, and logs).

Use the dashboard to:

- Add and configure providers
- Store API keys in the system keychain
- Enable/disable models
- Test chat with streaming responses

Sessions created before ownership was introduced have no `owner` field and
are treated as CLI/`local` only — HTTP API clients will not list or open them.

---

## 8. Connect MCP servers
Expand Down
4 changes: 2 additions & 2 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ 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 |
| Optional Bearer token auth (`config.yaml → server.api_key`) | medium |
| Bearer token auth on all HTTP routes (`server.api_token` / `ABBENAY_API_TOKEN`) | **done** |
| Rate limiting | low |

### Key files
Expand All @@ -133,7 +133,7 @@ aider, any `openai` SDK script, etc.). See DR-020.
### Milestones

- **M1**: `/v1/models` + `/v1/chat/completions` (streaming + non-streaming + tools) — **done**
- **M2**: Usage stats, optional auth
- **M2**: Usage stats, HTTP Bearer auth — **auth done** (usage stats remaining)
- **M3**: Rate limiting, `/v1/completions` (legacy)

---
Expand Down
45 changes: 45 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,3 +425,48 @@ default CMD uses `--grpc-tls`.
config, and tools. A warning alone is insufficient (finding C2). Fail-closed
startup forces an explicit security choice while preserving localhost DX and
allowing an escape hatch for trusted networks.

---

## DR-030: Secure-by-default HTTP API

**Date:** 2026-07-14
**Decision:** Require Bearer (or SameSite cookie) authentication on all HTTP
routes (`/api/*`, `/v1/*`, `/mcp`) by default, restrict CORS to an explicit
origin allowlist (never `*`), and bind the HTTP server to `127.0.0.1` by
default. Non-localhost bind requires explicit opt-in (`--host`,
`ABBENAY_HTTP_HOST`, or `server.host`). The API token resolves from
`ABBENAY_API_TOKEN` / `server.api_token` / `server.api_token_env`, or is
auto-generated and persisted as `http-api-token` in the config directory.
The dashboard uses `SameSite=Strict` cookies plus a CSRF token for browser
state-changing requests. Prefer `GET/POST /login` (token in the form/body)
over `/?token=` query login to avoid leaking credentials via history,
Referer, and access logs; the query form remains for compatibility and uses
a timing-safe compare. Cookies set the `Secure` flag when the request is
HTTPS or `X-Forwarded-Proto: https`. For local development only,
`ABBENAY_HTTP_AUTH=0` (or `false`/`off`/`no`/`disabled`) turns auth off and
logs a loud warning. Combining auth-disabled with a non-loopback bind
(`0.0.0.0`, LAN IP, etc.) fails closed: the HTTP server refuses to start.
**Rationale:** The previous defaults (no auth, `Access-Control-Allow-Origin: *`,
`app.listen(port)` → `0.0.0.0`) allowed any website the user visited to
cross-origin call the daemon and read/write secrets, config, chat, MCP, and
sessions. Secure-by-default closes that gap while keeping intentional network
exposure possible for containers with an explicit opt-in and a strong token.
An env-var escape hatch keeps local DX workable without baking an insecure
default back into production paths.

---

## DR-031: Session ownership principals

**Date:** 2026-07-14
**Decision:** Stamp every session with an `owner` principal and enforce
owner-scoped list/get/delete/chat on HTTP and gRPC. Principals are
`local` (CLI / local gRPC), `http:<token-fingerprint>` (HTTP API token, with
optional `X-Abbenay-Session-Owner` claim), or `consumer:<name>` (gRPC consumer
token). Legacy sessions without `owner` are treated as `local`. Cross-owner
access returns "not found".
**Rationale:** Authentication alone (DR-030) blocks anonymous access but does
not isolate sessions between authenticated principals sharing one daemon.
Ownership closes H9: HTTP clients, CLI, and named consumers cannot enumerate
or read each other's conversation history.
Loading
Loading