Skip to content

Operations Guide

This guide covers deploying, configuring, and operating a Diogenes instance for IT professionals and system administrators.


Prerequisites

  • Python 3.12+
  • PostgreSQL 14+ (with asyncpg driver)
  • Linux, macOS, or Windows with WSL

Installation

From Source

git clone https://github.com/ubiquitousthey/diogenes.git
cd diogenes
pip install -e ".[dev]"

Dependencies

Diogenes uses the following core dependencies:

Package Purpose
FastAPI Web framework and API
SQLAlchemy (async) Database ORM with asyncpg driver
Alembic Database migrations
Cryptography Cryptographic primitives
Pydantic Data validation and serialization
Jinja2 HTML template rendering
Argon2-cffi Password hashing
PyJWT JWT token management

Database Setup

PostgreSQL Configuration

Create a database for Diogenes:

createdb diogenes

Run Migrations

Diogenes uses Alembic for database schema management:

alembic upgrade head

Connection String

Configure the database URL via environment variable:

export DATABASE_URL="postgresql+asyncpg://user:password@localhost:5432/diogenes"

Configuration

Diogenes is configured via environment variables, managed by Pydantic Settings.

Core Settings

Variable Default Description
DATABASE_URL -- PostgreSQL connection string
ENFORCE_HTTPS true Redirect HTTP to HTTPS
SECRET_KEY -- Secret key for JWT signing

Rate Limiting

Variable Default Description
RATE_LIMIT_API_QUERY 100 Requests per minute per IP
RATE_LIMIT_KEY_REGISTRATION 10 Key registrations per hour per IP
RATE_LIMIT_ENDORSEMENT_OFFER 20 Endorsement offers per day per IP
RATE_LIMIT_ATTESTATION_EVENT 50 Attestation events per hour per IP
RATE_LIMIT_LOG_ANCHOR 12 Log head anchors per hour per IP
RATE_LIMIT_ATTESTATION_REQUEST_PER_TARGET 50 Attestation requests per hour per target
RATE_LIMIT_REQUEST_POLICY_PER_TARGET 20 Request-policy publications per hour per target

Graph Traversal

Variable Default Description
GRAPH_TRAVERSAL_TIMEOUT_SECONDS 10 BFS timeout for endorsement graph queries
GRAPH_TRAVERSAL_CACHE_TTL_SECONDS 60 Cache TTL for graph query results

Starting the Server

Development

./start.sh

Or directly with uvicorn:

uvicorn diogenes.server.app:create_app --factory --host 0.0.0.0 --port 8000 --reload

Production

For production deployments, use multiple workers and disable reload:

uvicorn diogenes.server.app:create_app --factory \
  --host 0.0.0.0 \
  --port 8000 \
  --workers 4 \
  --log-level info

Consider placing behind a reverse proxy (nginx, Caddy) for TLS termination and additional security headers.


Health Monitoring

Health Endpoint

curl http://localhost:8000/api/v1/health

Returns {"status": "ok", "version": "..."} when the server is running.

Log Chain Integrity

Periodically verify the transparency log chain:

curl -X POST http://localhost:8000/api/v1/log/verify

Returns {"valid": true, "entry_count": N, "errors": []} if the chain is intact.

Log Head

Monitor the log head for growth:

curl http://localhost:8000/api/v1/log/head

Backup and Recovery

Database Backup

Back up the PostgreSQL database regularly:

pg_dump diogenes > diogenes_backup_$(date +%Y%m%d).sql

Restore

psql diogenes < diogenes_backup_20260115.sql

Key Recovery

If a key holder loses access to their active key but has a backup, they can use key succession with recovery to transition to a new key. This requires the expired key's recovery credentials.


Security Hardening

Production Checklist

  • [ ] Enable HTTPS enforcement (ENFORCE_HTTPS=true)
  • [ ] Set a strong SECRET_KEY (use python -c "import secrets; print(secrets.token_urlsafe(32))")
  • [ ] Configure rate limits appropriate for your traffic patterns
  • [ ] Place behind a reverse proxy with TLS termination
  • [ ] Restrict database access to the application server only
  • [ ] Enable PostgreSQL SSL connections
  • [ ] Set up automated database backups
  • [ ] Monitor the health endpoint
  • [ ] Schedule periodic log chain verification

Firewall Rules

Only the following ports need to be exposed:

Port Service Access
443 HTTPS (reverse proxy) Public
8000 Uvicorn (if no proxy) Public or internal
5432 PostgreSQL Internal only

Temporal Anchoring

OpenTimestamps Setup

Temporal anchoring uses OpenTimestamps to anchor log hashes to Bitcoin. In Phase 0, a stub service is used. For production Bitcoin anchoring:

  1. Set DIOGENES_OTS_ENABLED=true. The opentimestamps client library is a main dependency, so no extra install step is needed.
  2. Configure the calendar servers via DIOGENES_OTS_CALENDAR_URLS (comma-separated).
  3. Anchor log heads periodically via POST /api/v1/log/anchor. The endpoint is operator-only (issue #501): send Authorization: Bearer <token> with a JWT signed by the operator key whose sub is the operator fingerprint. See POST /api/v1/log/anchor for the response shape and error codes.

Anchored entries include an ots_proof field that can be independently verified against the Bitcoin blockchain.

GET /api/v1/log/anchor-status stays public; poll it and act on its advisory anchor_due flag to decide when to call the anchor endpoint.

What an anchor status means

POST /api/v1/log/verify and the verification portal classify each proof into one of four states. The distinctions are the point — before #515 the code reported "verified" whenever proof bytes were present, without decoding them.

Status What it asserts What it does not assert
confirmed The proof parses, a Bitcoin attestation is reachable, and the block header at the attested height carries exactly the attested Merkle root. Reports the block height, block time, and which header source vouched for it. Nothing beyond the trustworthiness of that header source.
pending The proof parses; a calendar holds the commitment but it is not yet in a block. That anything is anchored yet.
unverifiable An anchor is present but cannot be confirmed here: no header source configured (the default), the source was unreachable, the per-request lookup budget was spent, or it is the legacy development stub proof. That the anchor is good or bad. It is an absence of evidence.
invalid The proof is provably not an anchor for this digest — it does not parse, or its attested Merkle root disagrees with the block header at that height. —

Only invalid is reported as an anchor error. unverifiable deliberately is not: POST /log/verify clears the durable verification checkpoint whenever an anchor error appears, so treating "no evidence" as an error would make every request re-scan the whole log.

Confirming anchors against Bitcoin

Confirmation requires a block header at the attested height. There is no header source by default, so every Bitcoin attestation reads unverifiable until you configure one:

DIOGENES_BITCOIN_HEADER_SOURCE_URL=https://blockstream.info/api
DIOGENES_BITCOIN_HEADER_SOURCE_TIMEOUT=5.0   # seconds
DIOGENES_OTS_HEADER_LOOKUP_BUDGET=8          # header lookups per verify request

Off by default on purpose: enabling it routes part of every verification request through a third-party block explorer, and confirmed is then only as trustworthy as that explorer. The status records header_source so the claim is never overstated. If you enable one, update the HTTPS-egress description in infra/vpc.tf to name it.

The lookup budget bounds how many header fetches one POST /log/verify may perform. That route is unauthenticated, so the bound is what keeps it from becoming an outbound-amplification vector. It counts fetches, not anchors: a request is charged only when a classification actually reached for a header, so with no header source configured — the default, where classification performs no network I/O at all — the budget is never spent. Past the budget the free, offline parse still runs, so a corrupt proof is still reported invalid; only an unresolved Bitcoin attestation truncates the pass, and a truncated verification is never written to the durable checkpoint.

Header sources are expected to speak the Esplora JSON dialect, which renders merkle_root in Bitcoin's conventional display order (reversed hex). Diogenes flips it back to the raw header order the attestation commits to before comparing; a custom source must return raw order.

Upgrading pending proofs

A pending proof only becomes evidence once the calendar folds it into a Bitcoin block. POST /api/v1/log/anchor will fetch that completion and merge it in — but only when explicitly enabled:

DIOGENES_OTS_UPGRADE_ENABLED=true   # default: false

POST /log/anchor is already operator-only (#501, see above), so this flag is not what stands between an anonymous caller and a calendar. Enable it when you want a completed proof merged in rather than left pending; leave it off if a calendar round-trip inside the request transaction is not acceptable in your deployment. With it off, the upgrade performs no network I/O at all.

Upgrading only ever contacts a calendar named in DIOGENES_OTS_CALENDAR_URLS (https only): the URI comes out of the proof, so it is attacker-influenced. A proof naming an unconfigured calendar is skipped, and the skip is reported in the response's upgrade_detail and logged at WARNING rather than silently looking like "not yet in a block". A re-anchor upgrades an existing proof and never overwrites it, so stored evidence survives.

Legacy stub proofs

Every log_entries.ots_proof written before real OTS was enabled holds b"ots-stub-proof", which is not an OpenTimestamps serialization. These are classified unverifiable — never invalid — so turning DIOGENES_OTS_ENABLED on against existing data produces zero anchor errors and no checkpoint churn. No migration or backfill is required.

Optionally, clear them first so those heads can be re-anchored for real. This is deliberately not automated:

UPDATE log_entries SET ots_proof = NULL WHERE ots_proof = '\x6f74732d737475622d70726f6f66';

Diagnosing "everything reads unverifiable"

GET /api/v1/log/anchor-status reports two signals for exactly this:

  • ots_library_available — false means the opentimestamps library is not importable in this image (a stripped install), so nothing can be classified.
  • bitcoin_header_source_configured — false means no header source is set, which is the default and the usual answer.

Known limitation

Without a header source, a proof whose Bitcoin attestation commits to the wrong digest is unverifiable, not invalid. That is structural: catching it requires a block header. It is never reported confirmed.


Troubleshooting

Common Issues

Symptom Cause Solution
503 Transparency log unreachable Database connection issue Check PostgreSQL is running and DATABASE_URL is correct
429 Too Many Requests Rate limit exceeded Wait for the rate limit window to reset, or adjust limits
HTTPS redirect loop Proxy not forwarding X-Forwarded-Proto Configure reverse proxy to pass protocol headers
Slow graph traversal Large endorsement graph Increase GRAPH_TRAVERSAL_TIMEOUT_SECONDS or reduce max_depth
Challenge expired errors Clock skew or slow client Ensure server and client clocks are synchronized (NTP)

Logs

Diogenes logs to standard output via Python's logging module. Configure log level via uvicorn's --log-level flag.