Zion Boggan zionboggan.com ↗

Add live registry deployment config

058a0bd   Zion Boggan committed on May 11, 2026 (1 month ago)
.env.example +27 -0
@@ -0,0 +1,27 @@
+# Oversight live registry configuration.
+# Copy to .env locally and fill in deployment-specific values. Do not commit .env.
+
+# Host ports. The registry stays loopback-only; Caddy owns public TLS.
+OVERSIGHT_REGISTRY_BIND=127.0.0.1
+OVERSIGHT_HTTP_BIND=0.0.0.0
+OVERSIGHT_HTTPS_BIND=0.0.0.0
+
+# Public hostnames. Point DNS at the host running the live Caddy profile.
+OVERSIGHT_REGISTRY_DOMAIN=registry.oversightprotocol.dev
+OVERSIGHT_BEACON_DOMAIN=b.oversightprotocol.dev
+OVERSIGHT_OCSP_DOMAIN=ocsp.oversightprotocol.dev
+OVERSIGHT_LICENSE_DOMAIN=lic.oversightprotocol.dev
+
+# Browser inspector origins allowed to read registry evidence endpoints.
+OVERSIGHT_CORS_ORIGINS=https://oversightprotocol.dev,https://www.oversightprotocol.dev,https://oversight-protocol.github.io
+
+# Live registry behavior.
+OVERSIGHT_JURISDICTION=GLOBAL
+OVERSIGHT_REKOR_ENABLED=0
+OVERSIGHT_REKOR_URL=https://log2025-1.rekor.sigstore.dev
+TRUSTED_PROXY=1
+
+# Secrets. Generate real values locally, for example:
+# openssl rand -hex 32
+OVERSIGHT_DNS_EVENT_SECRET=
+OVERSIGHT_OPERATOR_TOKEN=
CHANGELOG.md +18 -0
@@ -1,5 +1,23 @@
# Oversight CHANGELOG
+## Unreleased
+
+- **Live registry deployment config.** `docker-compose.yml` now has a `live`
+ Caddy profile with public TLS routing for the registry, beacon, OCSP-style,
+ and license-style hostnames. `Caddyfile` covers the full registry v1
+ read/evidence/tlog surface plus beacon routes, with all hostnames coming
+ from environment variables. `.env.example` documents public-safe defaults
+ and leaves secrets blank.
+- **Registry operator token.** The Python reference registry can now require
+ `OVERSIGHT_OPERATOR_TOKEN` for `POST /register` and `POST /attribute`.
+ The token is optional so local development and unauthenticated conformance
+ runs keep working, but production operators can protect write-side APIs
+ without changing route shapes. The conformance harness sends the token as
+ a bearer header when `OVERSIGHT_OPERATOR_TOKEN` is set.
+- **Deployment docs.** Added `docs/REGISTRY_DEPLOYMENT.md` covering the live
+ Compose/Caddy flow, route map, token headers, DNS bridge secret, and local
+ versus live conformance commands.
+
## v0.4.11 - 2026-05-08 Hardware-keys completion: Python parity, browser support, end-to-end seal
The `OSGT-HW-P256-v1` suite is now implemented end-to-end across all
Caddyfile +84 -30
@@ -1,53 +1,107 @@
-# OVERSIGHT registry - production Caddy config.
+# Oversight registry, production Caddy config.
#
-# Replace `oversightprotocol.dev` with the beacon domain you actually own
-# (the one whose hostname is baked into the beacon URLs you mint).
+# Set the hostnames in .env before starting the live Compose profile:
+# OVERSIGHT_REGISTRY_DOMAIN=registry.example.org
+# OVERSIGHT_BEACON_DOMAIN=b.example.org
+# OVERSIGHT_OCSP_DOMAIN=ocsp.example.org
+# OVERSIGHT_LICENSE_DOMAIN=lic.example.org
#
-# Beacons look like:
-# https://b.oversightprotocol.dev/p/{token}.png
-# https://ocsp.oversightprotocol.dev/r/{token}
-# https://lic.oversightprotocol.dev/v/{token}
-#
-# The simplest setup uses a single apex + path-based routing. Caddy auto-provisions
-# TLS via Let's Encrypt.
+# Caddy terminates TLS. The registry service stays private on the Compose
+# network and is reachable from the host only on the configured loopback bind.
-oversightprotocol.dev {
- encode gzip
+(registry_upstream) {
+ reverse_proxy oversight-registry:8765
+}
- # Attribution / evidence API - lock down with auth in production.
- handle /register {
- reverse_proxy oversight-registry:8765
+(security_headers) {
+ header {
+ X-Content-Type-Options nosniff
+ Referrer-Policy no-referrer
+ Permissions-Policy interest-cohort=()
}
- handle /attribute {
- reverse_proxy oversight-registry:8765
+}
+
+{$OVERSIGHT_REGISTRY_DOMAIN:registry.oversightprotocol.dev} {
+ encode zstd gzip
+ import security_headers
+
+ @registry_read path /health /.well-known/oversight-registry /evidence/* /tlog/* /candidates/semantic
+ handle @registry_read {
+ import registry_upstream
}
- handle /evidence/* {
- reverse_proxy oversight-registry:8765
+
+ @registry_write path /register /attribute
+ handle @registry_write {
+ import registry_upstream
}
- handle /health {
- reverse_proxy oversight-registry:8765
+
+ @dns_bridge path /dns_event
+ handle @dns_bridge {
+ import registry_upstream
}
- # Public beacon endpoints - open to the internet by design.
- handle /p/* {
- reverse_proxy oversight-registry:8765
+ handle {
+ respond 404
}
- handle /r/* {
- reverse_proxy oversight-registry:8765
+
+ log {
+ output file /data/registry-access.log {
+ roll_size 100mb
+ roll_keep 10
+ }
}
- handle /v/* {
- reverse_proxy oversight-registry:8765
+}
+
+{$OVERSIGHT_BEACON_DOMAIN:b.oversightprotocol.dev} {
+ encode zstd gzip
+ import security_headers
+
+ handle /p/* {
+ import registry_upstream
}
- # Everything else -> 404
handle {
respond 404
}
log {
- output file /data/access.log {
+ output file /data/beacon-access.log {
roll_size 100mb
roll_keep 10
}
}
}
+
+{$OVERSIGHT_OCSP_DOMAIN:ocsp.oversightprotocol.dev} {
+ encode zstd gzip
+ import security_headers
+
+ handle /r/* {
+ import registry_upstream
+ }
+
+ handle /ocsp/r/* {
+ import registry_upstream
+ }
+
+ handle {
+ respond 404
+ }
+}
+
+{$OVERSIGHT_LICENSE_DOMAIN:lic.oversightprotocol.dev} {
+ encode zstd gzip
+ import security_headers
+
+ handle /v/* {
+ import registry_upstream
+ }
+
+ handle /lic/v/* {
+ import registry_upstream
+ }
+
+ handle {
+ respond 404
+ }
+}
Dockerfile +1 -0
@@ -17,6 +17,7 @@ COPY registry/ ./registry/
# Persistent data volume
VOLUME ["/data"]
ENV OVERSIGHT_DB=/data/oversight-registry.sqlite
+ENV OVERSIGHT_DATA=/data
EXPOSE 8765
README.md +19 -0
@@ -48,6 +48,25 @@ pip install ".[formats]"
pip install ".[all]"
```
+## Live Registry Deployment
+
+The reference registry ships with a public-safe Compose/Caddy deployment path.
+Start `oversight-registry` on loopback for local operation, then enable the
+`live` profile when DNS is ready and Caddy should terminate TLS for the
+registry, beacon, OCSP-style, and license-style hostnames.
+
+```bash
+cp .env.example .env
+docker compose up -d oversight-registry
+docker compose --profile live up -d
+```
+
+Set `OVERSIGHT_DNS_EVENT_SECRET` and `OVERSIGHT_OPERATOR_TOKEN` in `.env`
+before exposing a public host. The operator token protects `POST /register`
+and `POST /attribute`; the DNS secret authenticates `/dns_event` bridge
+callbacks. Full route map and validation commands are in
+[`docs/REGISTRY_DEPLOYMENT.md`](docs/REGISTRY_DEPLOYMENT.md).
+
## Quick start
```bash
docker-compose.yml +33 -22
@@ -1,41 +1,52 @@
services:
oversight-registry:
build: .
- image: oversight-registry:0.1.0
+ image: oversight-registry:0.4.11
container_name: oversight-registry
restart: unless-stopped
ports:
- # bind to loopback only by default - put a reverse proxy (Caddy/nginx/Traefik)
- # in front to terminate TLS and reach the public beacon domain
- - "127.0.0.1:8765:8765"
+ # Loopback by default. Use the live Caddy profile for public TLS.
+ - "${OVERSIGHT_REGISTRY_BIND:-127.0.0.1}:8765:8765"
volumes:
- oversight_data:/data
environment:
OVERSIGHT_DB: /data/oversight-registry.sqlite
+ OVERSIGHT_DATA: /data
+ OVERSIGHT_CORS_ORIGINS: ${OVERSIGHT_CORS_ORIGINS:-https://oversightprotocol.dev,https://www.oversightprotocol.dev,https://oversight-protocol.github.io}
+ OVERSIGHT_DNS_EVENT_SECRET: ${OVERSIGHT_DNS_EVENT_SECRET:-}
+ OVERSIGHT_OPERATOR_TOKEN: ${OVERSIGHT_OPERATOR_TOKEN:-}
+ OVERSIGHT_JURISDICTION: ${OVERSIGHT_JURISDICTION:-GLOBAL}
+ OVERSIGHT_REKOR_ENABLED: ${OVERSIGHT_REKOR_ENABLED:-0}
+ OVERSIGHT_REKOR_URL: ${OVERSIGHT_REKOR_URL:-https://log2025-1.rekor.sigstore.dev}
+ TRUSTED_PROXY: ${TRUSTED_PROXY:-1}
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8765/health').read()"]
interval: 30s
timeout: 5s
retries: 3
- # Optional: Caddy TLS terminator + beacon domain fronting.
- # Uncomment once you have a real domain + DNS pointing at this host.
- #
- # caddy:
- # image: caddy:2-alpine
- # container_name: oversight-caddy
- # restart: unless-stopped
- # ports:
- # - "80:80"
- # - "443:443"
- # volumes:
- # - ./Caddyfile:/etc/caddy/Caddyfile:ro
- # - caddy_data:/data
- # - caddy_config:/config
- # depends_on:
- # - oversight-registry
+ caddy:
+ image: caddy:2-alpine
+ container_name: oversight-caddy
+ restart: unless-stopped
+ profiles: ["live"]
+ ports:
+ - "${OVERSIGHT_HTTP_BIND:-0.0.0.0}:80:80"
+ - "${OVERSIGHT_HTTPS_BIND:-0.0.0.0}:443:443"
+ environment:
+ OVERSIGHT_REGISTRY_DOMAIN: ${OVERSIGHT_REGISTRY_DOMAIN:-registry.oversightprotocol.dev}
+ OVERSIGHT_BEACON_DOMAIN: ${OVERSIGHT_BEACON_DOMAIN:-b.oversightprotocol.dev}
+ OVERSIGHT_OCSP_DOMAIN: ${OVERSIGHT_OCSP_DOMAIN:-ocsp.oversightprotocol.dev}
+ OVERSIGHT_LICENSE_DOMAIN: ${OVERSIGHT_LICENSE_DOMAIN:-lic.oversightprotocol.dev}
+ volumes:
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
+ - caddy_data:/data
+ - caddy_config:/config
+ depends_on:
+ oversight-registry:
+ condition: service_healthy
volumes:
oversight_data:
- # caddy_data:
- # caddy_config:
+ caddy_data:
+ caddy_config:
docs/REGISTRY_DEPLOYMENT.md +98 -0
@@ -0,0 +1,98 @@
+# Registry Deployment
+
+This is the public-safe live configuration for the reference Oversight
+registry. It keeps secrets in `.env`, keeps the registry process off the public
+host interface, and exposes TLS through Caddy.
+
+## Layout
+
+- `Dockerfile` builds the Python reference registry image.
+- `docker-compose.yml` runs the registry on loopback and adds a `live` profile
+ for Caddy.
+- `Caddyfile` routes the registry, beacon, OCSP-style, and license-style
+ hostnames to the registry container.
+- `.env.example` lists every live setting without carrying secret values.
+
+## First Run
+
+```bash
+cp .env.example .env
+# Fill OVERSIGHT_DNS_EVENT_SECRET and OVERSIGHT_OPERATOR_TOKEN in .env.
+# Use high-entropy random values; never commit .env.
+
+docker compose up -d oversight-registry
+curl http://127.0.0.1:8765/health
+```
+
+For public TLS, point DNS for the four configured hostnames at the host, then
+start the live profile:
+
+```bash
+docker compose --profile live up -d
+```
+
+## Public Routes
+
+`OVERSIGHT_REGISTRY_DOMAIN` serves the registry metadata, evidence, tlog, and
+operator routes:
+
+- `GET /health`
+- `GET /.well-known/oversight-registry`
+- `GET /evidence/{file_id}`
+- `GET /tlog/head`
+- `GET /tlog/proof/{index}`
+- `GET /tlog/range`
+- `GET /candidates/semantic`
+- `POST /register`
+- `POST /attribute`
+- `POST /dns_event`
+
+The beacon hostnames route only their beacon families:
+
+- `OVERSIGHT_BEACON_DOMAIN`: `/p/{token}.png`
+- `OVERSIGHT_OCSP_DOMAIN`: `/r/{token}` and `/ocsp/r/{token}`
+- `OVERSIGHT_LICENSE_DOMAIN`: `/v/{token}` and `/lic/v/{token}`
+
+Everything else returns `404`.
+
+## Operator Authentication
+
+If `OVERSIGHT_OPERATOR_TOKEN` is set, `POST /register` and `POST /attribute`
+require either:
+
+```http
+Authorization: Bearer <token>
+```
+
+or:
+
+```http
+X-Oversight-Operator-Token: <token>
+```
+
+Leaving `OVERSIGHT_OPERATOR_TOKEN` empty keeps the v1 conformance harness and
+local development behavior unchanged. Do not leave it empty on a public
+operator deployment.
+
+DNS bridge callbacks are separate. Set `OVERSIGHT_DNS_EVENT_SECRET`; the DNS
+bridge must send either `Authorization: Bearer <secret>` or
+`X-Oversight-DNS-Secret: <secret>` when posting `/dns_event`.
+
+## Conformance
+
+Local reference check:
+
+```bash
+python tests/test_registry_conformance.py
+```
+
+Live check against a token-protected registry:
+
+```bash
+OVERSIGHT_REGISTRY_URL=https://registry.example.org \
+OVERSIGHT_OPERATOR_TOKEN=<token> \
+python tests/test_registry_conformance.py
+```
+
+The token is read from the environment and sent as a bearer header. Do not put
+real token values in shell history on shared machines.
registry/server.py +25 -6
@@ -49,6 +49,7 @@ IDENTITY_PATH = DATA_DIR / "registry-identity.json"
TRUSTED_PROXY = bool(int(os.environ.get("TRUSTED_PROXY", "0")))
# When TRUSTED_PROXY=1, honor X-Forwarded-For for rate limiting.
DNS_EVENT_SECRET = os.environ.get("OVERSIGHT_DNS_EVENT_SECRET", "")
+OPERATOR_TOKEN = os.environ.get("OVERSIGHT_OPERATOR_TOKEN", "").strip()
# Rekor v2 wiring (v0.5 Session B). Off by default so existing tests do not
# generate live network traffic. Set OVERSIGHT_REKOR_ENABLED=1 to opt in.
@@ -399,6 +400,26 @@ def _rate_limit(request: Request):
raise HTTPException(429, "rate limit exceeded")
+def _bearer_or_header_token(request: Request, header_name: str) -> str:
+ supplied = request.headers.get(header_name, "")
+ if supplied:
+ return supplied.strip()
+ auth = request.headers.get("authorization", "")
+ if auth.lower().startswith("bearer "):
+ return auth[7:].strip()
+ return ""
+
+
+def _require_operator_auth(request: Request):
+ """Require the optional operator bearer token for write-side APIs."""
+ if not OPERATOR_TOKEN:
+ return
+ supplied = _bearer_or_header_token(request, "x-oversight-operator-token")
+ if hmac.compare_digest(supplied, OPERATOR_TOKEN):
+ return
+ raise HTTPException(401, "operator authentication required")
+
+
def _is_loopback_host(host: Optional[str]) -> bool:
if not host:
return False
@@ -411,11 +432,7 @@ def _is_loopback_host(host: Optional[str]) -> bool:
def _verify_dns_event_auth(request: Request):
"""Authenticate DNS bridge callbacks before trusting client_ip in the body."""
if DNS_EVENT_SECRET:
- supplied = request.headers.get("x-oversight-dns-secret", "")
- if not supplied:
- auth = request.headers.get("authorization", "")
- if auth.lower().startswith("bearer "):
- supplied = auth[7:].strip()
+ supplied = _bearer_or_header_token(request, "x-oversight-dns-secret")
if hmac.compare_digest(supplied, DNS_EVENT_SECRET):
return
raise HTTPException(401, "invalid DNS event secret")
@@ -480,6 +497,7 @@ def register(req: RegistrationRequest, request: Request):
another issuer's attribution record.
- A per-client rate limit applies.
"""
+ _require_operator_auth(request)
_rate_limit(request)
m = req.manifest
@@ -636,7 +654,8 @@ async def beacon_license(token_id: str, request: Request):
@app.post("/attribute")
-def attribute(q: AttributionQuery):
+def attribute(q: AttributionQuery, request: Request):
+ _require_operator_auth(request)
with db() as con:
row = None
if q.token_id:
tests/test_registry_conformance.py +18 -3
@@ -64,17 +64,32 @@ class Client:
"""Thin wrapper that presents the same get/post surface over a
FastAPI TestClient or a live httpx.Client."""
- def __init__(self, impl, base_url: str = ""):
+ def __init__(self, impl, base_url: str = "", default_headers: Optional[dict[str, str]] = None):
self._impl = impl
self._base = base_url.rstrip("/")
+ self._headers = default_headers or {}
+
+ def _merge_headers(self, kwargs: dict[str, Any]) -> dict[str, Any]:
+ if not self._headers:
+ return kwargs
+ merged = dict(self._headers)
+ merged.update(kwargs.pop("headers", {}) or {})
+ return {**kwargs, "headers": merged}
def get(self, path: str, **kwargs):
+ kwargs = self._merge_headers(kwargs)
return self._impl.get(self._base + path, **kwargs) if self._base else self._impl.get(path, **kwargs)
def post(self, path: str, **kwargs):
+ kwargs = self._merge_headers(kwargs)
return self._impl.post(self._base + path, **kwargs) if self._base else self._impl.post(path, **kwargs)
+def operator_headers() -> dict[str, str]:
+ token = os.environ.get("OVERSIGHT_OPERATOR_TOKEN", "").strip()
+ return {"Authorization": f"Bearer {token}"} if token else {}
+
+
def build_in_process_client():
"""Spin up the reference registry in a fresh temp data dir."""
from fastapi.testclient import TestClient
@@ -102,12 +117,12 @@ def build_in_process_client():
server.TLOG = TransparencyLog(server.TLOG_DIR, signing_key_hex=server.IDENTITY["ed25519_priv"])
tc = TestClient(server.app)
- return Client(tc), tmp, server.IDENTITY["ed25519_pub"]
+ return Client(tc, default_headers=operator_headers()), tmp, server.IDENTITY["ed25519_pub"]
def build_live_client(url: str):
import httpx
- return Client(httpx.Client(timeout=15.0), base_url=url), None, None
+ return Client(httpx.Client(timeout=15.0), base_url=url, default_headers=operator_headers()), None, None
# ---- Manifest fixture --------------------------------------------------------
tests/test_registry_unit.py +28 -1
@@ -187,6 +187,32 @@ def t4_evidence_bundle_can_attach_tlog_proofs():
print(" [PASS] evidence bundles attach tlog inclusion proofs for events")
+def t5_operator_token_gates_write_side_apis_when_configured():
+ original_token = registry_server.OPERATOR_TOKEN
+ try:
+ registry_server.OPERATOR_TOKEN = ""
+ registry_server._require_operator_auth(_fake_request("203.0.113.10"))
+
+ registry_server.OPERATOR_TOKEN = "operator-secret"
+ registry_server._require_operator_auth(
+ _fake_request("203.0.113.10", {"authorization": "Bearer operator-secret"})
+ )
+ registry_server._require_operator_auth(
+ _fake_request("203.0.113.10", {"x-oversight-operator-token": "operator-secret"})
+ )
+ try:
+ registry_server._require_operator_auth(
+ _fake_request("203.0.113.10", {"authorization": "Bearer wrong"})
+ )
+ except HTTPException as exc:
+ assert exc.status_code == 401
+ else:
+ raise AssertionError("wrong operator token should be rejected")
+ finally:
+ registry_server.OPERATOR_TOKEN = original_token
+ print(" [PASS] optional operator token gates write-side APIs")
+
+
def main():
print("=" * 60)
print(" registry.server - focused unit tests")
@@ -195,8 +221,9 @@ def main():
t2_register_rejects_unsigned_sidecar_mismatch()
t3_dns_event_requires_secret_for_non_loopback()
t4_evidence_bundle_can_attach_tlog_proofs()
+ t5_operator_token_gates_write_side_apis_when_configured()
print()
- print(" ALL TESTS PASSED - 4/4")
+ print(" ALL TESTS PASSED - 5/5")
if __name__ == "__main__":