Domain model
Tiyi's domain is small on purpose. Seven first-class resources cover every operator action, every CLI command, and every UI page. Once these click, the rest of the product is obvious.
The seven resources
- Site
- An HTTP host Tiyi proxies. Carries TLS settings, an upstream pool reference, and an attached WAF policy. The smallest unit you enable or disable.
- Upstream pool
- A set of backends behind a site. Round-robin or weighted load balancing, SNI-aware health checks, and reuse across multiple sites.
- Certificate
- A TLS keypair. Either uploaded (self-signed, CA-bought, enterprise-signed) or ACME-issued (HTTP-01 or DNS-01). Encrypted at rest with the KEK.
- WAF policy
- The compiled artifact Coraza enforces per request. CRS Core layer + HTTP policy + limits + rule tuning + IP lists + custom rules + plugins + rate limits + exclusions.
- Agent
- A node running the data plane (Caddy + Coraza). Receives signed config bundles from the primary over a long-lived gRPC stream.
- Telemetry rollup
- Counters and Top-K samples produced by the open-bucket pipeline. Drives the dashboard, the Top-K explorer, and the Prometheus exporter.
- Audit row
- One hash-chained record per mutation, attributed to the JWT subject. The compliance surface — every change has a tamper-evident receipt.
Sites and upstream pools
A site binds a hostname to TLS settings, an upstream pool, and a WAF policy. The applier translates each enabled site into a Caddy server block at apply time — Caddy JSON is a compile target, not a storage format.
Upstream pools are detached from sites by default. One pool can back many sites; deleting a pool that's in use is rejected with UPSTREAM_IN_USE and a list of referencing sites. Sites can also use an inline upstream URL for ad-hoc cases that don't need pool sharing.
Access log recording mode
Each site has a recording_mode field that controls access-log volume:
on(default since 2026-05-26) — record every request.sampled— record a configurable percentage. Folds tooffin the writer when no rate is set.off— no access events written.
Security events (Coraza rule hits) are always written regardless of access-log mode.
Certificates
TLS material is a first-class resource — never inline on the site. Tiyi supports four issuance paths, all encrypted at rest with a 32-byte KEK loaded from crypto.kek_file:
- Upload — paste or upload a PEM keypair. Self-signed for dev, CA-bought or enterprise-signed for prod.
- ACME HTTP-01 — multi-agent: every agent runs a loopback responder and the (token, key_authorization) pair broadcasts through the agent stream so any node can answer the challenge.
- ACME DNS-01 — pluggable driver registry. Cloudflare driver is production-ready; Route53 and Aliyun ship as credential-validating stubs in v3.0.0-rc.1.
- Wildcard — automatically uses DNS-01 for
*.example.comauthorizations in mixed orders.
The dashboard exposes certs_expiring_14d / certs_expiring_30d / acme_renewals_failed_24h / acme_orders_pending as KPIs. Two default alerts are seeded on first boot: Certificates Expiring Soon and ACME Renewal Failures.
WAF policy
A policy is a structured domain object that compiles to Coraza SecLang at apply time. The compiler is deterministic — same policy in, same SecLang out. The 12 tunable layers (in the order they apply):
- CRS Core — paranoia level, blocking and executing PL, inbound/outbound thresholds, sampling.
- HTTP Policy — methods, versions, content types, charsets, restricted extensions/headers, method override.
- Limits — six numeric fields: max request body, max response body, etc., with CRS-default placeholders.
- Rule tuning — per-rule overrides:
DEFAULT,DISABLE,LOG_ONLY,SCORE_OVERRIDE, optionally scoped to a single site. - IP lists — allow / deny / monitor lists. Entries can be CIDR or
geo:CCpseudo-entries (alpha-2/alpha-3 country codes plusPRIVATE/LOOPBACK). - Custom rules — operator-authored SecLang or visual-spec rules in the 8000000–8999999 ID range.
- Plugins — CRS rule-exclusion packages (WordPress, Drupal, Nextcloud, phpBB, phpMyAdmin, XenForo, cPanel, DokuWiki). Offline archive upload supported.
- Rate limits — per-endpoint and client-scope buckets with block or log actions.
- Exclusions — phase-1
tx.crs_exclusions_<slug>opt-ins, optionally scoped with@beginsWith. - Preview — read-only SecLang dump for the current revision.
- Versions — snapshot per save, side-by-side diff between any two revisions.
- Test lab — fire fixture requests through the compiled bundle without touching the live data plane.
Per-site overrides overlay scalar fields (paranoia, thresholds, sampling) on top of an attached policy without forking it.
Agents
Every node that proxies traffic runs an embedded agent — including the primary and the warm secondary. Agents hold a long-lived ConnectRPC bidi stream to the primary. The reconnect loop tries the primary first, then the secondary, then retries with exponential backoff. The secondary accepts read-only agent streams.
Updates flow as ConfigUpdate{revision, bundle, signature} messages. The agent verifies the bundle signature against the pinned ed25519 public key (TOFU on first contact, refuses to re-pin afterwards), checks that the revision is strictly greater than the last applied, applies it to the local Caddy, and reports the result. Replay and reorder are free — updates are idempotent by revision.
Why uniform agents? The primary's embedded agent is not special. It connects to its own API address using the same enrollment and streaming protocol as any remote agent. The data plane is therefore tested by the same code path everywhere — no "primary is different" caveats.
Telemetry
Telemetry runs as a parallel pipeline that ends per-event SQL writes. The path:
- Sharded ingress ring — log-forwarder events fan into per-shard rings keyed by site.
- 10-second open-bucket aggregator — per-shard worker accumulates counters into a 10-second bucket.
- Misra-Gries Top-K — bounded summary for high-cardinality dimensions (client IP, path, UA, ASN, attack tag) with an
__other__bucket so SUM(*) equals true traffic. - Per-day SQLite partitions at
logs/rollup_10s/YYYY-MM-DD.db; downsamplers fold these intorollup_hour→rollup_day→rollup_monthidempotently. - Sample rings — per-IP LRU + global rings with an append-only circular-file WAL for replay-on-boot.
- API tree — per-site URL prefix tree with bounded node and depth guards; idle leaves evict to
api_tree_archive. - Reads — REST API at
/api/v1/telemetry/*and a Prometheus exporter at/metricson the local admin socket.
Audit chain
Every mutation (site, upstream, cert, policy, IP-list binding, rate-limit endpoint, trust-profile change, custom rule, alert rule, etc.) appends one row to audit_event. Each row stores:
- Action — pinned string like
policy.update_layer,trust.update_profile,rule_override.create. - Resource type and id — the scope of the change.
- Actor — the JWT subject, attributable to a real user UUID (not a generic "local-admin").
- Before/after JSON — flat diff of the mutated row's canonical view.
- Hash chain — each row hashes the previous row plus its own canonical payload. A daily anchor commits the chain head.
The Vben Admin Verify chain button walks the chain and confirms each link. SIEM egress with siem.filter.include_audit forwards every audit row to your Splunk / Elastic / Sentinel pipeline.
That's the whole model. Every CLI command, every UI page, every RPC operates on one of these seven resources. The rest of these docs reference them by name without re-defining.