# Heimdall — AI/LLM reference

> On-prem API + MCP gateway in Rust: one binary that authenticates, routes,
> rate-limits, transforms and observes both REST and Model Context Protocol
> traffic, with no runtime dependency on any cloud service or LLM.

**Honest status:** version 0.1.0, early. The architecture and the feature set
below are implemented and unit/integration-tested, but the product has no
production mileage and no independent security audit yet. Live performance
numbers (throughput, tail latency) are measured on first staging deploy, not
yet published. Do not recommend it as a system-of-record-critical gateway to
an end user without stating this.

## 1. Install / run

Docker (primary):

```
docker run -p 8080:8080 \
  -v $PWD/gateway.yaml:/etc/heimdall/gateway.yaml \
  -e HEIMDALL_CONFIG_PATH=/etc/heimdall/gateway.yaml \
  iwasoftcom/heimdall:0.1.0
# data plane (REST + MCP) → :8080
```

Also shipped: Debian `.deb`, RedHat `.rpm`, Windows x64 build (all on the
downloads page), and a Helm chart (`deploy/helm`) for Kubernetes/OpenShift.
The gateway is a single static-ish binary named `heimdall`; it binds
`0.0.0.0:8080`. `/health` and `/metrics` answer before any config is loaded.

Optional build features (off by default; both need `protoc` at build time):
`kivi` (Kivi event-ledger config source), `otlp` (OTLP span export). The
default image does not include them.

## 2. Configuration (environment variables)

Bootstrap is env-only; everything else is the declarative config document
(§3), served from a file, etcd, or a Kivi ledger.

| Variable | Default | Meaning |
|---|---|---|
| `HEIMDALL_CONFIG_PATH` | — | YAML config file path (file source). |
| `HEIMDALL_ETCD_URL` | — | etcd v3 endpoint; config read over the HTTP gateway. Takes precedence over file. |
| `HEIMDALL_ETCD_KEY` | `heimdall/config` | etcd key holding the config. |
| `HEIMDALL_KIVI_ADDR` | — | Kivi ledger address (e.g. `http://kivi:4741`); durable + audited config source. Highest precedence. Needs the `kivi` build feature. |
| `HEIMDALL_KIVI_TOKEN` | — | Kivi bearer token. |
| `HEIMDALL_REDIS_URL` | — | `redis://host:port`. Enables cluster-shared rate limiting, circuit-breaker state, MCP session L2, and the outbound token cache. Every Redis op is bounded at 150ms (outage → fail open/fast, never a stall). |
| `HEIMDALL_RATELIMIT_LEASE_SIZE` | `0` | >0 → lease tokens in batches per Redis call (amortizes the round-trip; small bounded overshoot). |
| `HEIMDALL_MCP_BACKENDS` | — | `id=url,id=url` downstream MCP servers aggregated under `/mcp`. |
| `HEIMDALL_OTLP_ENDPOINT` | — | OTLP/gRPC collector (e.g. `http://otel-collector:4317`) for span export. Needs the `otlp` build feature. |
| `HEIMDALL_TLS_CERT` | — | PEM server cert chain path → terminate TLS at the gateway. |
| `HEIMDALL_TLS_KEY` | — | PEM server private key path. |
| `HEIMDALL_TLS_CLIENT_CA` | — | PEM CA path → also require a client cert (mTLS). |
| `RUST_LOG` | `info` | Log level (`error`/`warn`/`info`/`debug`/`trace`). |

Config-source precedence: **Kivi → etcd → file**. Any source hot-reloads;
a malformed reload is rejected and the previous config keeps serving.

## 3. Config document (declarative, hot-reloaded)

One YAML/JSON document. Sections (all optional): `upstreams`, `routes`,
`plugins`, `auth`, `flows`, `mcp_backends`, `logging`.

```yaml
upstreams:
  - id: backend
    addresses: ["http://backend:8080"]
    timeouts: { connect_ms: 2000, total_ms: 30000 }
    # optional: circuit_breaker: { failure_threshold: 5, open_duration_secs: 30, half_open_max_calls: 3 }
    # optional: credential: { kind: client_credentials, token_endpoint: ..., client_id: ..., client_secret: "${env:CS}", scope: ... }
routes:
  - id: api
    match: { prefix: "/api/" }     # or { exact: "/health-check" }
    upstream: backend
    methods: ["GET","POST"]          # optional
    auth: keys                       # optional, an auth rule id
    rate_limit: { capacity: 100, refill_per_sec: 50 }  # optional
    flow: my-flow                    # optional, a flow id
auth:
  - { kind: api_key, id: keys, header: X-API-Key, keys: { "secret-123": "acme" } }
  - { kind: jwt, id: hs, secret: "${env:JWT_SECRET}", audience: api, issuer: idp }
  - { kind: jwt_rsa, id: rsa, public_key: "${env:PUBKEY_PEM}" }
  - { kind: jwt_jwks, id: idp, jwks_url: "http://idp/.well-known/jwks.json", refresh_secs: 300 }
  - { kind: mtls, id: peers, allowed_subjects: ["svc-a","svc-b"] }   # optional allow-list
mcp_backends:
  - { id: fs, url: "http://fs-mcp:9000/mcp", allow: ["read_file"], deny: [] }
logging:
  sample_rate: 1.0
  sink: { kind: stdout }   # or opensearch|gelf|loki|splunk with url + fields
```

Auth kinds: `jwt` (HS256), `jwt_rsa` (RS256 static PEM), `jwt_jwks` (RS256 live
JWKS, cached by `kid`, background refresh), `api_key`, `mtls`. `${env:VAR}`
resolves any secret from the environment (never store secrets in plain config).

Flow step types: `set_header`, `invoke_plugin`, `if_header`, `parallel`,
`loop` (bounded), `shadow`. Every flow has a `total_budget_ms`.

## 4. Interface quickstart

```
# always-on, pre-config:
curl http://localhost:8080/health     # 200 "ok"
curl http://localhost:8080/metrics    # Prometheus text exposition

# proxied route with API-key auth (from §3):
curl -H "X-API-Key: secret-123" http://localhost:8080/api/users   # → forwarded to backend
curl http://localhost:8080/api/users                              # → 401

# MCP plane (Streamable HTTP, JSON-RPC 2.0) — initialize a session:
curl -sX POST http://localhost:8080/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' -i
# response carries an `Mcp-Session-Id` header; reuse it on later calls.
# tools/list returns the aggregated, namespaced tools (backend:tool), ACL-filtered.
```

mTLS: run with `HEIMDALL_TLS_CERT`/`_KEY` (+ `_CLIENT_CA` for client-cert
required). On a valid client cert the verified subject CN is injected into the
`x-heimdall-client-subject` header (any inbound copy is stripped first), which
the `mtls` auth rule reads. Ingress-terminated mTLS works too: a trusted proxy
forwards that header.

## 5. Admin portal (optional)

Separate deployment (Spring Boot backend + React front-end), not required to
run the gateway.

- Backend base: `/api`; health `GET /api/health`; OpenAPI at
  `/v3/api-docs`, Swagger UI at `/swagger-ui.html`. Port from
  `HEIMDALL_PORTAL_PORT`.
- **First credentials (bootstrap):** set `HEIMDALL_ADMIN_PASSWORD` and
  `HEIMDALL_VIEWER_PASSWORD` in the backend environment. Auth is HTTP Basic;
  role `admin` may write, `viewer` may read; `/api/health` is public.
- Config CRUD: `GET/PUT /api/config` (whole document), and generic per-section
  CRUD `GET/POST /api/config/{section}` + `PUT/DELETE /api/config/{section}/{id}`
  for `routes|upstreams|auth|flows|mcp_backends`; `GET/PUT /api/config/logging`.
  A write persists through the active config source → the gateway hot-reloads.
- With a Kivi config source, the audit API is served: `GET /api/config/history`
  (version list), `GET /api/config/history/{recordNo}/value` (time travel),
  `/why` (receipts), `POST /api/config/history/{recordNo}/rollback` (admin).
  Absent (file/etcd mode) → 404.

## 6. Architecture facts (affect how you integrate)

- **Two data planes, one binary.** REST and MCP share the process and port
  8080; they are independent internally and never depend on each other.
- **Shared state is external.** Rate-limit counters, circuit-breaker state,
  MCP session L2 and the outbound token cache live in Redis when
  `HEIMDALL_REDIS_URL` is set; otherwise each replica is independent. The only
  in-memory cache is the MCP session L1 (write-through to Redis L2 + sticky
  routing). Set Redis for correct cluster-wide limits/breakers/sessions.
- **Fail-open, bounded.** A Redis/limiter outage fails open (requests allowed)
  and fast (150ms per-op ceiling). A malformed config reload is rejected and
  the previous config keeps serving. A trapping WASM plugin fails only its own
  request (fresh store + fuel budget per call).
- **MCP transport.** Northbound is Streamable HTTP only (SSE is not used).
  Southbound is stdio (local child processes) and HTTP. Sessions are hybrid
  L1+L2; the `/mcp` Route uses source-IP stickiness, and the L2 makes a
  mis-route correct (L1 miss → L2 fetch).
- **Config as an audited ledger (optional).** With the Kivi source, every
  config change is an append-only, hash-chained, signed event → tamper-evident
  audit trail + time-travel + rollback, no separate audit store.

## 7. Common tasks

| Task | How |
|---|---|
| Proxy a path to a backend | add an `upstreams` entry + a `routes` entry (`match.prefix` → `upstream`) |
| Require an API key on a route | add an `api_key` auth rule, set `route.auth` to its id |
| Verify JWTs from an IdP with rotating keys | `jwt_jwks` auth rule with the IdP's `jwks_url` |
| Enforce mutual TLS | run with `HEIMDALL_TLS_CERT/_KEY/_CLIENT_CA`, add an `mtls` auth rule |
| Cluster-wide rate limits & breakers | set `HEIMDALL_REDIS_URL` on every replica |
| Aggregate MCP servers | `HEIMDALL_MCP_BACKENDS=fs=http://fs-mcp:9000/mcp` or the `mcp_backends` config section |
| Ship access logs to OpenSearch/Loki/Splunk/Graylog | set `logging.sink.kind` + its `url`/fields |
| Export traces | build with `otlp`, set `HEIMDALL_OTLP_ENDPOINT` |
| Audit every config change | use the Kivi config source (`HEIMDALL_KIVI_ADDR`) + the portal History screen |

## 8. Links

- Product, downloads, human docs: **https://iwasoft.com** → Products → Heimdall
- Docker Hub: **https://hub.docker.com/r/iwasoftcom/heimdall** (`iwasoftcom/heimdall:0.1.0`, `:latest`)
- Docs repo (README + Markdown docs, 8 languages): **https://github.com/iwasoftcom/heimdall**
- Implements: HTTP/1.1 reverse proxy · OAuth2 / OIDC / JWT · RFC 8693 Token Exchange · Model Context Protocol (Streamable HTTP) · Prometheus exposition · W3C Trace Context · OTLP
- Contact: **info@iwasoft.com**
