Add static API-key auth, dockerize service, publish façade docs

- sms/auth.py: verify_api_key dependency (Bearer/X-API-Key, SHA-256 hash
  compare via secrets.compare_digest, fail-closed 503 if no hash). All routes
  gated via a protected router; /health stays open for probes.
- config.py: new sms_api_key_hash setting (VOIPMS_SMS_API_KEY_HASH).
- Dockerfile + .dockerignore + docker-compose.yml: lean python:3.13-slim
  image; secrets injected via env_file, never baked in; host-localhost-only
  port mapping (SMS_PORT override); /health healthcheck.
- .env.example: committed template (placeholders only, no secrets).
- sms/docs/sms-facade.dokuwiki.txt: abstraction API reference (published to
  the apidoc wiki at voipms:sms-facade).
- README: Authentication section, Docker section, 401/503 error rows.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
chelsea
2026-07-04 05:09:43 +00:00
parent db5bde0a70
commit 1d9e7e0fab
9 changed files with 359 additions and 9 deletions

View File

@@ -13,6 +13,7 @@ with a per-day outbound send guard.
| `models.py` | Pydantic request/response models |
| `client.py` | `VoipMsSMSClient` — async `httpx` wrappers for the 7 methods |
| `guard.py` | `DailySendCounter` — UTC-day counter + send logging |
| `auth.py` | Static API-key dependency (SHA-256 hash compare, fail-closed) |
| `app.py` | FastAPI app exposing the operations as REST endpoints |
| `requirements.txt` | Dependencies |
@@ -30,6 +31,7 @@ with a per-day outbound send guard.
```sh
export VOIPMS_API_USERNAME=...
export VOIPMS_API_PASSWORD=...
export VOIPMS_SMS_API_KEY_HASH=$(python3 -c "import secrets,hashlib;print(hashlib.sha256(secrets.token_urlsafe(32).encode()).hexdigest())")
# optional overrides:
# export VOIPMS_BASE_URL=https://voip.ms/api/v1/rest.php
# export VOIPMS_DIALING_MODE=nanpa # or e164
@@ -39,6 +41,32 @@ export VOIPMS_API_PASSWORD=...
(`Settings` also reads a `.env` file in the working directory.)
## Authentication
Every endpoint except `/health` requires a static API key. Send it as either:
```
Authorization: Bearer <your-api-key>
X-API-Key: <your-api-key>
```
The server stores only the **SHA-256 hash** of accepted keys in
`VOIPMS_SMS_API_KEY_HASH` (comma-separated list allowed, for rotation). The raw
key is never persisted — generate one, hash it, and keep the raw key wherever
your clients live:
```sh
python3 -c "import secrets,hashlib; k=secrets.token_urlsafe(32); print('KEY:',k); print('HASH:',hashlib.sha256(k.encode()).hexdigest())"
```
Put the `HASH:` value in `VOIPMS_SMS_API_KEY_HASH`; hand the `KEY:` value to
your calling services. To rotate, add the new hash alongside the old, switch
callers over, then remove the old hash.
If `VOIPMS_SMS_API_KEY_HASH` is empty/unset, the service is **fail-closed**:
protected routes return `503` until a hash is configured. `/health` stays open
for liveness probes.
## Run
```sh
@@ -48,6 +76,29 @@ uvicorn sms.app:app --reload
Open `http://127.0.0.1:8000/docs` for the Swagger UI.
## Run with Docker
A `Dockerfile` and `docker-compose.yml` live at the repo root. The compose
stack builds the image and runs it, injecting secrets from the local `.env` via
`env_file`**secrets are never baked into the image** (`.dockerignore` excludes
`.env`, `.venv`, `.git`, `sms/docs/`).
```sh
docker compose up -d # build + start
docker compose logs -f sms # follow logs
docker compose ps # status / healthcheck
docker compose down # stop
```
The container binds `0.0.0.0:8000` internally; compose maps it to
**`127.0.0.1:${SMS_PORT:-8000}`** on the host — localhost-only by default. To
expose on the LAN, set the mapping to `8000:8000` (access is still gated by the
`VOIPMS_SMS_API_KEY_HASH` API key). If host port 8000 is taken, set
`SMS_PORT=8001` (or any free port) in `.env`.
A healthcheck polls `/health` every 30s. The image is lean — reference docs and
the virtualenv are excluded; only `sms/` + dependencies are inside.
## Endpoints
| Method | Path | Upstream | Guarded |
@@ -72,6 +123,9 @@ type; the message id must be 0 when set). For `/sms` the id query param is
| Condition | HTTP | Body |
| --- | --- | --- |
| Missing API key | 401 | `{"detail":"missing api key"}` |
| Invalid API key | 401 | `{"detail":"invalid api key"}` |
| No API key hash configured | 503 | `{"detail":"api key auth not configured"}` |
| Daily limit exceeded | 429 | `{detail, limit, used_today}` |
| voip.ms returns non-success status | 502 | upstream message |
| Auth / IP-whitelist failure | 500 | `upstream authentication failure` (details logged server-side) |