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.
- Node
- A data plane running Caddy + Coraza. Every deployment has one built-in local node; licensed deployments can add remote Agents that receive signed bundles from the Controller.
- 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.4.
- 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. At apply time Tiyi compiles Coraza SecLang for CRS and custom rules, and a separate compressed native IP-list artifact for CIDR/geo:CC matching. Both compilers are deterministic. 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). Country and CIDR expansions compile into the nativetiyi_iplisthandler ahead of Coraza; they are never expanded into SecLang. Overlapping allow bindings take precedence over deny. - 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 overlays layer scalar fields (paranoia, thresholds, sampling) plus site-scoped rule overrides, custom rules, IP-list bindings, and rate-limit endpoints on top of an attached policy without forking it.
Local node and remote Agents
The Controller always owns one built-in local data plane. It applies the same signed, validated configuration in-process, but it is not enrolled, persisted, heartbeated, or commanded as an Agent. The Nodes page shows it first as This machine · Local · Built in.
Remote Agents hold one long-lived ConnectRPC stream to the configured Controller URL. Each Agent verifies the bundle signature against its pinned ed25519 public key, rejects replay or reordering, applies the accepted bundle to local Caddy, and reports the result. If the Controller is unavailable, the Agent reconnects with backoff while continuing to proxy with its last accepted bundle.
One topology, no role switch. Adding or removing remote Agents changes the node count only. The Controller does not change modes or roles, and remote continuity does not make the Controller highly available.
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.