# Secure the Gateway

> Optional TDAI_GATEWAY_API_KEY Bearer auth, TDAI_CORS_ORIGINS allow-list, non-loopback host warnings, and Hermes client MEMORY_TENCENTDB_GATEWAY_API_KEY alignment.

- Repository: TencentCloud/TencentDB-Agent-Memory
- GitHub: https://github.com/TencentCloud/TencentDB-Agent-Memory
- Human docs: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a
- Complete Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-5a33bbf5540a/llms-full.txt

## Source Files

- `README.md`
- `src/gateway/config.ts`
- `src/gateway/server.ts`
- `hermes-plugin/memory/memory_tencentdb/client.py`
- `hermes-plugin/memory/memory_tencentdb/README.md`

---

---
title: "Secure the Gateway"
description: "Optional TDAI_GATEWAY_API_KEY Bearer auth, TDAI_CORS_ORIGINS allow-list, non-loopback host warnings, and Hermes client MEMORY_TENCENTDB_GATEWAY_API_KEY alignment."
---

The standalone **TdaiGateway** (`src/gateway/server.ts`) is an optional Node HTTP sidecar for Hermes (default `127.0.0.1:8420`). Security is **opt-in**: by default all routes are open and no CORS headers are sent. Operators enable a shared-secret Bearer gate with `TDAI_GATEWAY_API_KEY` / `server.apiKey`, restrict browser origins with `TDAI_CORS_ORIGINS` / `server.corsOrigins`, and align the Hermes Python client with `MEMORY_TENCENTDB_GATEWAY_API_KEY` (same secret, client-side only).

<Warning>
Auth and CORS both default **off**. Existing loopback-only deployments keep working without changes. Bind to a non-loopback address (`0.0.0.0`, LAN IP) without `TDAI_GATEWAY_API_KEY` only if you intentionally accept unauthenticated access to capture, recall, search, and seed.
</Warning>

## Security model

| Control | Server config | Env | Default | Effect |
| :--- | :--- | :--- | :--- | :--- |
| Bearer auth | `server.apiKey` | `TDAI_GATEWAY_API_KEY` | unset (disabled) | When set, every route **except** `GET /health` and `OPTIONS` requires `Authorization: Bearer <key>` |
| CORS allow-list | `server.corsOrigins` | `TDAI_CORS_ORIGINS` (comma-separated) | `[]` | Empty → **no** `Access-Control-Allow-*` headers; browsers block cross-origin calls |
| Bind address | `server.host` | `TDAI_GATEWAY_HOST` | `127.0.0.1` | Loopback-only by default; non-loopback without apiKey triggers a loud startup WARN |
| Port | `server.port` | `TDAI_GATEWAY_PORT` | `8420` | HTTP listen port |

Resolution for the shared secret: env `TDAI_GATEWAY_API_KEY` overrides `server.apiKey` from the config file. CORS is inverted: an explicit yaml/json `server.corsOrigins` (including `[]`) wins over `TDAI_CORS_ORIGINS` so shell leakage cannot force CORS on.

Config file discovery (first hit wins): `TDAI_GATEWAY_CONFIG` path → `./tdai-gateway.yaml` or `./tdai-gateway.json` in CWD → same names under the data dir (`TDAI_DATA_DIR` / default `~/.memory-tencentdb/memory-tdai`).

## Enable Bearer auth

### Gateway side

Set the same secret the clients will send. Prefer env for secrets; yaml is fine if the file is not world-readable.

```bash
export TDAI_GATEWAY_API_KEY="replace-with-a-long-random-secret"
export TDAI_GATEWAY_HOST="127.0.0.1"   # keep loopback unless you need remote clients
export TDAI_GATEWAY_PORT="8420"
```

Or in `tdai-gateway.yaml` / `tdai-gateway.json`:

```yaml
server:
  host: "127.0.0.1"
  port: 8420
  apiKey: "${TDAI_GATEWAY_API_KEY}"   # whole-string ${VAR} leaves expand from the process env
```

String leaves that are exactly `${VAR_NAME}` expand from `process.env` at load time (missing vars become empty strings).

### Route gate behavior

When `server.apiKey` is a non-empty string:

| Request | Auth required? | Failure |
| :--- | :--- | :--- |
| `GET /health` | No | — |
| `OPTIONS *` (CORS preflight) | No | Returns `204` after CORS header application |
| `POST /recall`, `/capture`, `/search/memories`, `/search/conversations`, `/session/end`, `/seed` | Yes | HTTP **401** |
| Any other path after auth | — | HTTP **404** if path unknown |

Header rules (`checkAuth`):

1. Missing or non-`Bearer ` `Authorization` → `{"error":"Unauthorized: missing Bearer token"}` (401).
2. Present token compared with `crypto.timingSafeEqual` after UTF-8 buffer length check → wrong token → `{"error":"Unauthorized: invalid token"}` (401).
3. Leading/trailing spaces on the provided token are stripped before compare; empty after strip is invalid.

When `apiKey` is unset, `checkAuth` is a no-op (legacy open mode).

### Call a protected route

```bash
curl -sS -H "Authorization: Bearer $TDAI_GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"preferences","session_key":"agent:main:main"}' \
  http://127.0.0.1:8420/recall
```

Health stays open for orchestrators:

```bash
curl -sS http://127.0.0.1:8420/health
# {"status":"ok"|"degraded","version":"…","uptime":…,"stores":{…}}
```

## Hermes client alignment

The Hermes `memory_tencentdb` provider is an HTTP **client**. Auth on the wire is independent of whether the Gateway process was spawned by the supervisor, Docker, or systemd.

### Env names

| Variable | Who reads it | Role |
| :--- | :--- | :--- |
| `TDAI_GATEWAY_API_KEY` | Gateway (`loadGatewayConfig`) | Enables enforcement + secret to match against |
| `MEMORY_TENCENTDB_GATEWAY_API_KEY` | Hermes provider / SDK client only | Outbound `Authorization: Bearer …` on every request (including `GET /health`) |
| `TDAI_GATEWAY_API_KEY` (fallback) | Hermes `_resolve_gateway_api_key()` | Used only if `MEMORY_TENCENTDB_GATEWAY_API_KEY` is unset — shared env-file convenience |

The Gateway **never** reads `MEMORY_TENCENTDB_GATEWAY_API_KEY`. That name is plugin-side only.

### Resolution order (Hermes)

`_resolve_gateway_api_key()` walks:

1. `MEMORY_TENCENTDB_GATEWAY_API_KEY` (strip whitespace; empty → try next)
2. `TDAI_GATEWAY_API_KEY`
3. Neither set → no `Authorization` header (matches open Gateway)

`MemoryTencentdbSdkClient` strips the key again and attaches `Authorization: Bearer <key>` from `_build_headers` on both GET and POST.

### Supervisor does not inject the secret

`GatewaySupervisor` accepts `api_key` for the **client** half only. When it `Popen`s the Node sidecar it copies `os.environ` and deliberately **does not** set `TDAI_GATEWAY_API_KEY` from that client argument. Consequences:

- If both processes already share an env file that sets `TDAI_GATEWAY_API_KEY`, the child inherits enforcement automatically.
- If you only set `MEMORY_TENCENTDB_GATEWAY_API_KEY`, the client sends Bearer tokens but the Gateway stays open unless you also configure Gateway-side auth (env, yaml, Docker `-e`, unit file).

```bash
# Both ends — same secret
export TDAI_GATEWAY_API_KEY="replace-with-a-long-random-secret"
export MEMORY_TENCENTDB_GATEWAY_API_KEY="$TDAI_GATEWAY_API_KEY"
# Or set only TDAI_GATEWAY_API_KEY once when Hermes and Gateway share the process environment
```

Provider config metadata exposes `gateway_api_key` as optional/secret with `env_var: MEMORY_TENCENTDB_GATEWAY_API_KEY`.

## CORS allow-list

### Defaults

- `corsOrigins: []` → `applyCorsHeaders` returns immediately; **no** CORS response headers.
- Browsers then apply same-origin policy and block cross-origin XHR/fetch.
- Non-browser clients (Hermes Python, `curl`, in-process tools) are unaffected by CORS.

### Configure

```bash
export TDAI_CORS_ORIGINS="https://app.example.com,https://admin.example.com"
```

```yaml
server:
  corsOrigins:
    - "https://app.example.com"
    - "https://admin.example.com"
  # or comma-separated string: "https://a,https://b"
  # or [] to force CORS off even if TDAI_CORS_ORIGINS is set in the shell
```

### Match behavior

| Config | Response headers |
| :--- | :--- |
| `[]` (default) | None |
| `["*"]` | `Access-Control-Allow-Origin: *`, methods `GET, POST, OPTIONS`, headers `Content-Type, Authorization`; startup WARN |
| Explicit list, request `Origin` in list | Echo that origin + methods/headers + `Vary: Origin` |
| Explicit list, origin missing or not listed | No allow-origin headers; `Vary: Origin` only |

`OPTIONS` always completes with **204** after CORS application (no body). Allowed methods/headers are fixed as above when CORS is enabled.

## Startup security posture

On listen success, `logSecurityPosture()` logs one summary line (never logs the key material):

```text
Security posture: auth=ENABLED (Bearer)|disabled host=<host> cors=no-headers|wildcard(*)|allowlist(N)
```

Additional warnings:

| Condition | Log |
| :--- | :--- |
| Auth disabled | WARN: `TDAI_GATEWAY_API_KEY is NOT set` — routes open to anyone who can reach the port |
| Host not loopback (`127.0.0.1` / `localhost` / `::1`) **and** auth disabled | Second WARN naming the bind host and listing sensitive routes |
| `corsOrigins` contains `*` | WARN: every browser origin can call the gateway |

Loopback classification is exact string equality on the configured host, not a network interface scan.

## Recommended deployment patterns

<Tabs>
  <Tab title="Local Hermes (loopback)">
Keep defaults: `TDAI_GATEWAY_HOST=127.0.0.1`, no apiKey, empty CORS. Sufficient when only the local Hermes process talks to the sidecar.

```bash
# Optional hardening even on localhost
export TDAI_GATEWAY_API_KEY="$(openssl rand -hex 32)"
export MEMORY_TENCENTDB_GATEWAY_API_KEY="$TDAI_GATEWAY_API_KEY"
```
  </Tab>
  <Tab title="Docker / remote bind">
Official Hermes Docker docs default `TDAI_GATEWAY_HOST=0.0.0.0` so the port is reachable inside the network namespace. Treat that as a **non-loopback** bind: enable Bearer auth and do not publish the port publicly without a reverse proxy or network policy.

```bash
export TDAI_GATEWAY_HOST="0.0.0.0"
export TDAI_GATEWAY_API_KEY="<secret>"
export MEMORY_TENCENTDB_GATEWAY_API_KEY="<secret>"
# Prefer internal Docker network + curl health from the same container
```
  </Tab>
  <Tab title="Browser UI calling Gateway">
Set a concrete `server.corsOrigins` allow-list (not `*`). Clients must still send Bearer if auth is enabled. Prefer loopback or private network + TLS terminator in front of the Node process; the Gateway itself speaks plain HTTP.
  </Tab>
</Tabs>

## Enable checklist

<Steps>
  <Step title="Choose bind host">
Prefer `127.0.0.1` unless remote clients require otherwise. Non-loopback without auth logs a loud WARN at startup.
  </Step>
  <Step title="Set Gateway secret">
Export `TDAI_GATEWAY_API_KEY` or set `server.apiKey` in `tdai-gateway.yaml` / `.json`. Restart the Gateway process so `loadGatewayConfig` reloads.
  </Step>
  <Step title="Align Hermes client">
Set `MEMORY_TENCENTDB_GATEWAY_API_KEY` to the **same** secret (or rely on shared `TDAI_GATEWAY_API_KEY` fallback). Do not assume the supervisor copies the client `api_key` into the child env.
  </Step>
  <Step title="Configure CORS only if browsers call the API">
Leave empty for sidecar-only use. For browser UIs, list exact origins; avoid `*`.
  </Step>
  <Step title="Verify">
Confirm startup line `auth=ENABLED (Bearer)`, health without token, protected route without token returns 401, protected route with token succeeds.
  </Step>
</Steps>

## Verification

```bash
# 1) Health open
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8420/health
# expect 200

# 2) Protected route without token
curl -sS -o /dev/null -w "%{http_code}\n" \
  -H "Content-Type: application/json" \
  -d '{"query":"x","session_key":"s"}' \
  http://127.0.0.1:8420/recall
# expect 401 when auth enabled

# 3) Protected route with token
curl -sS -o /dev/null -w "%{http_code}\n" \
  -H "Authorization: Bearer $TDAI_GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"x","session_key":"s"}' \
  http://127.0.0.1:8420/recall
# expect 200 (or 400 only if body invalid after auth)
```

Inspect Gateway logs for:

```text
Security posture: auth=ENABLED (Bearer) host=127.0.0.1 cors=no-headers
```

Hermes client failures on auth show as HTTP errors from the SDK (`Gateway … returned 401: …`) and feed the provider circuit breaker after repeated failures.

## Failure modes

| Symptom | Likely cause | Fix |
| :--- | :--- | :--- |
| `401 Unauthorized: missing Bearer token` | Client not sending header | Set `MEMORY_TENCENTDB_GATEWAY_API_KEY` or pass `Authorization: Bearer` |
| `401 Unauthorized: invalid token` | Secret mismatch, trailing newline, wrong env var | Align secrets; both sides strip whitespace, but different strings still fail |
| Hermes capture/recall fails after enabling Gateway auth | Client-only key unset | Export Hermes key; supervisor does not auto-enable Gateway enforcement from client `api_key` |
| Gateway still open after setting only `MEMORY_TENCENTDB_GATEWAY_API_KEY` | Gateway does not read that name | Set `TDAI_GATEWAY_API_KEY` / `server.apiKey` on the Gateway process |
| Browser CORS errors | Empty allow-list or origin not listed | Add exact origin to `server.corsOrigins` / `TDAI_CORS_ORIGINS` |
| Loud WARN on Docker start | `0.0.0.0` without apiKey | Expected; set `TDAI_GATEWAY_API_KEY` before publishing the port |
| `OPTIONS` works but POST blocked by browser | Origin not echoed | Confirm request Origin is literally in the allow-list (no trailing slash surprises) |

## What this page does not cover

- OpenClaw **in-process** plugin path (no Gateway HTTP auth — hooks run inside the OpenClaw process).
- TLS / mTLS, reverse-proxy auth, or network policies (operator responsibility outside this package).
- LLM, embedding, or Tencent VectorDB API keys (`TDAI_LLM_*`, embedding `apiKey`, `tcvdb.apiKey`) — separate secrets from Gateway Bearer auth.

## Related pages

<CardGroup cols={2}>
  <Card title="Gateway HTTP API" href="/gateway-http-api">
    Route inventory, request/response fields, auth exceptions, and error envelope.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full `TDAI_*` / `MEMORY_TENCENTDB_*` catalog and client vs server API-key names.
  </Card>
  <Card title="Gateway lifecycle" href="/gateway-ops">
    Start/stop/status/health with `memory-tencentdb-ctl` and `~/.memory-tencentdb` layout.
  </Card>
  <Card title="Hermes setup" href="/hermes-setup">
    Install provider, auto-discovery, and health verification before enabling auth.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Auth 401, circuit breaker, and log/probe checklist.
  </Card>
</CardGroup>
