DHCP management and failover platform for Technitium DNS Server.
Tessera monitors DHCP health through distributed voter agents and automatically fails over to a standby server when consensus is reached.
git clone https://github.com/ArtificialFoundry/tessera.git
cd tessera
mkdir -p config data/backupsGenerate your secrets:
# Technitium API token (from your Technitium dashboard)
echo "your-technitium-api-token" > config/token
chmod 600 config/token
# Admin key for Tessera write operations
openssl rand -hex 32 > config/admin-keyDefine your DHCP servers in config/servers.json:
[
{"name": "dns-1", "url": "https://192.0.2.1:53443", "role": "active", "priority": 0},
{"name": "dns-2", "url": "https://192.0.2.2:53443", "role": "candidate", "priority": 10}
]Start with Docker:
docker compose up -dOpen http://localhost:8780 — you should see the dashboard.
Voters are lightweight health checkers deployed on your infrastructure VMs. You need at least 3 voters to form a quorum.
From the dashboard (easiest):
- Go to Voters → click Generate Token
- Follow the wizard — set an optional IP restriction and expiry
- Copy the one-time token shown at the end
- On each voter machine, run:
curl -O https://your-tessera/voter/tessera-install-voter.sh
sudo bash tessera-install-voter.sh \
--tessera-url http://tessera-host:8780 \
--auto-register \
--registration-token <paste-token-here>That's it. The installer auto-detects the hostname, registers with Tessera, receives a PSK, installs the voter script, and starts a 30-second health check timer.
With Docker (on the voter machine):
# After registration, you'll have a voter.conf with VOTER_NAME and VOTER_PSK
docker run -d \
--name tessera-voter \
--network host \
--cap-add NET_RAW --cap-add NET_ADMIN \
--restart unless-stopped \
-v /path/to/voter.conf:/etc/tessera/voter.conf:ro \
tessera-voter:latest
--network host+ capabilities are needed for the DHCP broadcast probe.
Back on the dashboard, go to Failover — you should see voter cards appearing
with HTTP ✓ and DHCP ✓ badges.
Voters (deployed on 3+ VMs)
├── HTTP probe → Technitium API responsive?
├── DHCP probe → nmap broadcast — actually serving leases?
└── POST /api/v1/vote (HMAC-signed, every 30s)
Tessera
├── Collects votes, evaluates quorum
├── 3 consecutive DOWN rounds → failover to candidate
├── 5 consecutive UP rounds → failback to original
└── Syncs scopes, backups config, enforces drift
Vote logic: overall status is "up" if either check passes. Both individual
results (http_status, dhcp_status) are shown as badges in the dashboard.
| Page | What it does |
|---|---|
| Failover | Live voter grid, health check badges, quorum bar, transition history |
| DHCP | Scope management, reservations, leases |
| Protection | Backups, drift enforcement, restore points |
| Servers | Add/remove/promote/demote DHCP servers |
| Voters | Registry, token wizard, approve/revoke/rotate keys |
Admin actions (anything that writes) prompt for your admin key on first use. The key is stored in your browser tab and cleared when you close it.
Each voter reads /etc/tessera/voter.conf:
VOTER_NAME="voter-1"
VOTER_PSK="hmac-psk-hex-string"
TESSERA_URL="http://tessera-host:8780"
CHECK_TIMEOUT="5"| Variable | Required | Default | Description |
|---|---|---|---|
VOTER_NAME |
Yes | — | Unique voter identifier |
VOTER_PSK |
Yes | — | HMAC-SHA256 signing key (hex) |
TESSERA_URL |
Yes | — | Tessera API base URL |
CHECK_TIMEOUT |
No | 5 |
HTTP probe timeout (seconds) |
DHCP_TIMEOUT |
No | CHECK_TIMEOUT |
DHCP broadcast probe timeout (seconds) |
DHCP_INTERFACE |
No | auto-detect | Network interface for DHCP probe |
Tip: Increase
DHCP_TIMEOUTon VMs where DHCP broadcasts cross VLANs or where CPU contention causes occasional timeouts (e.g.,DHCP_TIMEOUT="10").
uv sync --all-extras
# Backend
uv run uvicorn tessera.app:create_app --factory --reload --port 8780
# Frontend (separate terminal)
cd frontend && npm ci && npm run dev
# Quality checks
uv run pytest # 274 tests
uv run ruff check src/ tests/ # Lint
uv run ruff format --check src/ tests/ # Format
uv run mypy src/ # Type check (strict)| Document | Description |
|---|---|
| Architecture | System design, engine graph, source tree |
| Internals | Engine lifecycle, state machines, data flows, middleware |
| Deployment | Production deployment, systemd, reverse proxy |
| Development | Local setup, testing, conventions |
| Improvement Plan | Security and resilience roadmap |
| Changelog | Release history |
| Contributing | How to contribute |
| Security | Vulnerability reporting |
Server configuration (servers.json)
Tessera supports N DHCP servers with three roles:
| Role | Description |
|---|---|
active |
Currently serving DHCP leases |
candidate |
Ready to be promoted on failover (ranked by priority) |
observer |
Monitored for health but never promoted |
[
{"name": "dns-1", "url": "https://192.0.2.1:53443", "role": "active", "priority": 0},
{"name": "dns-2", "url": "https://192.0.2.2:53443", "role": "candidate", "priority": 10},
{"name": "dns-mon", "url": "https://192.0.2.4:53443", "role": "observer", "priority": 99}
]Per-server tokens override the global Technitium API token:
{"name": "dns-3", "url": "...", "role": "candidate", "priority": 20, "token": "per-server-token"}Admin authentication
Set TESSERA_ADMIN_API_KEY in your environment or compose file:
export TESSERA_ADMIN_API_KEY=$(openssl rand -hex 32)Read endpoints are unauthenticated. Write endpoints require Authorization: Bearer <key>:
# Read (no auth)
curl http://tessera:8780/api/v1/servers
# Write (auth required)
curl -X POST http://tessera:8780/api/v1/servers/dns-2/promote \
-H "Authorization: Bearer <your-key>"The dashboard prompts for the key on first write action and stores it in sessionStorage.
Voter self-registration API
Generate a token:
curl -X POST http://tessera:8780/api/v1/voters/tokens \
-H "Authorization: Bearer <admin-key>" \
-H "Content-Type: application/json" \
-d '{"bind_ip": "192.168.1.0/24", "ttl": 3600}'Register a voter:
curl -X POST http://tessera:8780/api/v1/voters/register \
-H "Content-Type: application/json" \
-d '{"token": "<one-time-token>", "name": "voter-4"}'Token options:
bind_ip— restrict to IP/CIDR (persists on voter record, enforced on every vote)ttl— expiry in seconds (0 = never expires)
If TESSERA_AUTO_APPROVE_VOTERS=true, the PSK is returned immediately.
Otherwise, approve via dashboard or POST /api/v1/voters/{name}/approve.
Hot-reload configuration
Tessera watches these files and reloads without restart:
| File | Effect |
|---|---|
voters.json |
New/removed voter keys take effect immediately |
servers.json |
Server pool updated |
| API token file | Token rotation without restart |
Send SIGHUP for immediate reload: kill -HUP $(pidof uvicorn)
Settings that require restart: TESSERA_PORT, TESSERA_HOST, engine parameters (quorum, rounds, intervals).
Manual voter setup (without registration)
If you prefer manual PSK management instead of the registration API:
- Generate a PSK:
openssl rand -hex 32 - Add it to
config/voters.json:
{
"voter-1": "generated-psk-hex",
"voter-2": "another-psk-hex"
}- Create
/etc/tessera/voter.confon the voter machine with the matching PSK - Install the voter script:
sudo cp voter/tessera-voter.sh /usr/local/bin/
sudo chmod 755 /usr/local/bin/tessera-voter.sh
sudo cp voter/tessera-voter.service voter/tessera-voter.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now tessera-voter.timerBare-metal Tessera (without Docker)
uv sync --frozen
sudo mkdir -p /etc/tessera /var/lib/tessera/backups
TESSERA_SERVERS_FILE=/etc/tessera/servers.json \
TESSERA_ADMIN_API_KEY=$(cat config/admin-key) \
uv run uvicorn tessera.app:create_app --factory --host 0.0.0.0 --port 8780Environment variables reference
All settings use the TESSERA_ prefix.
| Variable | Default | Description |
|---|---|---|
TESSERA_SERVERS_FILE |
/etc/tessera/servers.json |
Path to servers JSON |
TESSERA_API_TOKEN_FILE |
/etc/tessera/token |
Technitium API token file |
TESSERA_VOTER_KEYS_FILE |
/etc/tessera/voters.json |
Voter HMAC PSK file |
TESSERA_ADMIN_API_KEY |
— | Admin Bearer token |
TESSERA_PORT |
8780 |
HTTP port |
TESSERA_HOST |
0.0.0.0 |
Bind address |
TESSERA_QUORUM |
3 |
Minimum votes for quorum |
TESSERA_FAILOVER_ROUNDS |
3 |
Consecutive failed rounds before failover |
TESSERA_FAILBACK_ROUNDS |
5 |
Consecutive healthy rounds before failback |
TESSERA_VOTE_TTL |
90 |
Vote expiry (seconds) |
TESSERA_VOTERS |
— | Expected voter names (comma-separated) |
TESSERA_SYNC_INTERVAL |
300 |
Scope sync interval (seconds) |
TESSERA_BACKUP_DIR |
/var/lib/tessera/backups |
Backup directory |
TESSERA_MAX_BACKUPS |
50 |
Max retained backups |
TESSERA_BACKUP_CRON_SCHEDULE |
— | Cron for auto-backup |
TESSERA_ENFORCEMENT_INTERVAL |
300 |
Drift check interval (seconds) |
TESSERA_CONFIG_RELOAD_INTERVAL |
10 |
Config watch interval (seconds) |
TESSERA_REGISTRATION_TOKEN_TTL |
3600 |
Default token TTL (seconds) |
TESSERA_PSK_GRACE_PERIOD |
60 |
PSK rotation grace period (seconds) |
TESSERA_AUTO_APPROVE_VOTERS |
false |
Auto-approve registrations |
TESSERA_VOTER_REGISTRY_FILE |
/var/lib/tessera/voter-registry.json |
Voter metadata |
TESSERA_CA_CERT_FILE |
— | Custom CA cert for Technitium HTTPS |
TESSERA_CORS_ORIGINS |
— | CORS allowed origins |
TESSERA_DEBUG |
false |
Debug logging |
API reference
All endpoints under /api/v1/. Write endpoints require Authorization: Bearer <admin-key>.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/ping |
— | Health check |
GET |
/health |
— | Engine health |
POST |
/auth/verify |
Bearer | Verify admin token |
GET |
/status |
— | Failover status + voter states |
POST |
/vote |
HMAC | Submit health vote |
GET |
/servers |
— | List servers |
POST |
/servers |
Bearer | Add server |
DELETE |
/servers/{name} |
Bearer | Remove server |
POST |
/servers/{name}/promote |
Bearer | Promote to active |
POST |
/servers/{name}/demote |
Bearer | Demote to candidate |
GET |
/scopes |
— | List DHCP scopes |
GET |
/scopes/{name} |
— | Scope details |
POST |
/scopes |
Bearer | Create scope |
PUT |
/scopes/{name} |
Bearer | Update scope |
DELETE |
/scopes/{name} |
Bearer | Delete scope |
POST |
/scopes/{name}/enable |
Bearer | Enable scope |
POST |
/scopes/{name}/disable |
Bearer | Disable scope |
GET |
/leases |
— | All leases |
GET |
/leases/{scope} |
— | Leases for scope |
GET |
/backups |
— | List backups |
POST |
/backups |
Bearer | Manual backup |
POST |
/voters/tokens |
Bearer | Generate reg token |
GET |
/voters/tokens |
— | List tokens |
DELETE |
/voters/tokens/{prefix} |
Bearer | Delete token |
POST |
/voters/register |
— | Register voter |
GET |
/voters |
— | List voters |
GET |
/voters/pending |
— | Pending voters |
POST |
/voters/{name}/approve |
Bearer | Approve voter |
POST |
/voters/{name}/revoke |
Bearer | Revoke voter |
DELETE |
/voters/{name} |
Bearer | Delete voter |
POST |
/voters/{name}/rotate-key |
Bearer | Rotate PSK |