567 lines
39 KiB
Markdown
567 lines
39 KiB
Markdown
# Administration API
|
||
|
||
The API is served on the separate administration listener, normally
|
||
`http://127.0.0.1:9090`. It is never mounted on reverse-proxy HTTP/TLS listeners.
|
||
Loopback HTTP is the default. Settings can enable administration HTTPS, choose a
|
||
certificate and persist a non-loopback bind; plain remote HTTP requires the explicit
|
||
startup `--allow-remote-admin` gate. Responses carry no-store, CSP and other browser
|
||
protection headers.
|
||
|
||
## Browser documentation and anonymous surface
|
||
|
||
Open `/docs` **on the administration listener** after signing in. The default **Reader**
|
||
shows the bundled project Markdown documentation; the separate **API** tab renders the
|
||
OpenAPI/Swagger interface. The documentation bundle, OpenAPI JSON, JavaScript, styles and
|
||
Swagger vendor assets all require the same session. API writes are opt-in and CSRF-protected.
|
||
See [API_ACCESS.md](API_ACCESS.md).
|
||
|
||
Only GET/HEAD `/`, the panel routes `/overview`, `/vhosts`, `/streams`,
|
||
`/acl-profiles`, `/web-server`, `/configuration`, `/tls`, `/cache`, `/logging`, `/settings`,
|
||
`/assets/login.js`, `/assets/login-langs.json`, `/assets/style.css`, `/favicon.svg`,
|
||
`/healthz`, `/api/auth/status`, and POST `/api/auth/setup`, `/api/auth/login` are
|
||
anonymous. Panel routes serve the same guarded shell as `/`, so a browser refresh
|
||
preserves the selected subpage instead of falling back to the landing page. Root
|
||
optionally authenticates and selects login or operational HTML. `/healthz` is exactly `{"status":"ok"}`.
|
||
Unknown/private paths and method mismatches return a generic 401 without a session.
|
||
Only top-level `/docs` and `/docs/` navigations redirect to `/?next=docs`; API/schema
|
||
fetches never redirect to login HTML. Other invalid Host/Origin checks can return 403.
|
||
|
||
## Authentication
|
||
|
||
| Method / path | Input or result |
|
||
|---|---|
|
||
| `GET /api/auth/status` | Public `setup_required` boolean only; no version or operational state |
|
||
| `POST /api/auth/setup` | One-time `{token, username, password}`; creates no session |
|
||
| `POST /api/auth/login` | `{username, password}`; sets session cookie and returns session data |
|
||
| `GET /api/auth/session` | Username, CSRF token, absolute expiry and idle timeout |
|
||
| `POST /api/auth/logout` | Revoke the current session |
|
||
| `POST /api/auth/logout-all` | Revoke all administrator sessions |
|
||
| `POST /api/auth/password` | `{current_password, new_password}`; changes password and revokes sessions |
|
||
|
||
Mutations, including setup/login, require `Origin` equal to the active administration scheme plus `{Host}` and
|
||
`X-RustProxy-Request: 1`. Authenticated mutations also need `X-CSRF-Token` from
|
||
session data. Send JSON as `application/json`. A permanent bearer access token
|
||
is not supported; the old setup capability does not authorize API reads/writes.
|
||
|
||
The `rustproxy_session` cookie is HttpOnly, SameSite=Strict and Path=/ with the configured
|
||
7–30 day absolute lifetime (default 30 days); server-side revocation still applies. HTTPS
|
||
administration adds the Secure attribute; behind a trusted HTTPS-terminating reverse proxy
|
||
the external forwarded scheme controls that flag. A protected SSH/VPN tunnel remains valid,
|
||
while direct remote administration should use HTTPS plus firewall/private-network policy.
|
||
Direct IP/localhost Host values must use the actual administration port. A DNS name explicitly
|
||
listed in `administration.allowed_hosts` represents the browser-facing authority and may use
|
||
the trusted reverse proxy's external port. Foreign Origin/cross-site requests are rejected.
|
||
There is no permissive CORS mode.
|
||
Auth request bodies are limited to 4096 bytes.
|
||
|
||
Typical errors include 401 for missing/invalid credentials, 403 for origin/CSRF
|
||
rejection, 409 for already-completed setup, 422 for password policy and 429 for
|
||
throttling/busy workers. Error objects usually contain an `error` code/string;
|
||
framework extraction failures are not guaranteed to have that JSON shape.
|
||
|
||
## Configuration and observations
|
||
|
||
| Method / path | Result |
|
||
|---|---|
|
||
| `GET /healthz` | Public process health/version; not a backend health probe |
|
||
| `GET /api/config` | `{revision, config}` |
|
||
| `GET /api/config/history` | Retained revision metadata; `?revision=N` returns that document plus the previous retained document |
|
||
| `POST /api/config/history/restore` | Input `{revision, expected_revision}`; validates the older retained document and publishes it as a new configuration revision |
|
||
| `PUT /api/config` | Input `{expected_revision, config}`; returns the applied envelope; retained for atomic multi-section changes |
|
||
| `PUT /api/config/{section}` | Input `{expected_revision, value}`; replaces one validated section and returns the applied envelope |
|
||
| `GET /api/backends` | Authenticated per-vhost backend observations and active/desired frontend plans |
|
||
| `GET /api/status` | Uptime, active revision, configured counts, listener addresses, feature flags, aggregate metrics, active/desired TCP/UDP streams with per-stream L4 counters, active/desired administration state, TLS policy/cipher inventory and structured-log sink counters |
|
||
|
||
`status.streams` contains `active`, `desired`, `restart_required` and a `metrics` map keyed by startup-active stream ID. Stream counters include active/total/completed sessions, rejects, errors, RX/TX bytes and UDP packet counts; the dashboard derives rates from byte-counter deltas.
|
||
| `GET /api/logs` | Recent bounded access entries, ring capacity and contention-drop count |
|
||
| `GET /api/audit` | Last 100 audit entries; database retention is 10,000 |
|
||
| `GET /api/events` | SSE `snapshot` events containing status and logs |
|
||
| `GET /metrics` | Prometheus text exposition |
|
||
| `GET /api/metrics` | Alias of `/metrics` |
|
||
|
||
Except for health and authentication/bootstrap routes, these endpoints require a
|
||
session. These administrative previews are not anonymous. The separate configurable
|
||
exporter uses its own CIDRs and optional bearer token; see the scrape section below.
|
||
|
||
SSE has eight viewer slots, generates a snapshot approximately every two seconds
|
||
and rechecks session validity. It does not queue an unlimited per-viewer event log
|
||
or guarantee replay. The browser falls back to polling. An active dashboard renews
|
||
idle activity, not the absolute session deadline.
|
||
|
||
Configuration history retains the newest 512 successful manual/configuration revisions in SQLite schema 8. `GET /api/config/history` is session-protected like the rest of the administration API; `limit` is bounded to 512 and `revision` selects one retained revision for diff/inspection. `POST /api/config/history/restore` also requires the normal CSRF/request-marker protections, rejects stale `expected_revision`, and never rewinds the revision counter: restoring revision 85 while current revision is 120 creates revision 121.
|
||
|
||
Configuration updates return 409 for a stale revision, 422 for validation failure
|
||
and 429 when the eight-operation update admission bound is reached. Persistence and
|
||
snapshot publication finish before a successful response. Disconnecting the caller
|
||
after the detached writer starts does not cancel an accepted write.
|
||
The document limit is 256 KiB, with an additional 1024-byte request-envelope allowance.
|
||
For ordinary Settings saves prefer `PUT /api/config/{section}`. Supported sections are
|
||
`listeners`, `response`, `cache`, `logging`, `forwarding`, `headers`, `prometheus`, `geoip`,
|
||
`administration`, `tls`, `vhosts`, `web-services`, `redirects`, `not_found_hosts`, `streams`, `acl-profiles` and `host-policies`.
|
||
Each section write still uses revision CAS, full validation and atomic persistence.
|
||
Only a change to the derived cache-compatibility identity purges hot/cold cache;
|
||
unrelated section saves do not.
|
||
|
||
## Configuration fields
|
||
|
||
The canonical document uses `vhosts`, `web_services`, `redirects`, `listeners`, `administration`, `tls`, `logging`,
|
||
`prometheus`, `geoip_updates`, `cache`, `compression`, `server_header`, `headers`, `host_policies` and other defaulted
|
||
settings. Unknown fields are rejected. `backends`/`routes` at the top level are
|
||
input-only legacy migration fields, omitted from saved canonical documents.
|
||
|
||
```json
|
||
{
|
||
"vhosts": [{
|
||
"id": "application",
|
||
"frontend": {
|
||
"hosts": ["app.localhost", "alias.localhost"],
|
||
"listeners": [{"address":"127.0.0.1:8080", "tls":false, "http2":false}]
|
||
},
|
||
"backends": [{"id":"application", "url":"http://127.0.0.1:9000", "allow_private":true}],
|
||
"routes": [{"id":"root", "path_prefix":"/"}]
|
||
}]
|
||
}
|
||
```
|
||
|
||
The backend service must already exist. Routes without `backend_id` select from
|
||
the vhost pool; an explicit ID pins that path to a member of the same pool.
|
||
`vhosts[].force_ssl=true` redirects matching cleartext requests with HTTP 308 to the
|
||
same hostname/path/query. Force SSL requires both a selected plain HTTP listener and a
|
||
selected TLS listener. Port 443 is preferred when several TLS ports are selected;
|
||
otherwise multiple non-standard TLS ports are rejected as an ambiguous redirect target.
|
||
`vhosts[].forward_client_ip` defaults to true. Set it to false only when an application
|
||
must see RustProxy's transport peer instead of the visitor: backend requests then omit
|
||
`Forwarded`, `X-Forwarded-For` and `X-Real-IP`, while `X-Forwarded-Host/Proto/Port/URI`
|
||
remain and RustProxy/Tinyauth still use the real client address. See examples and
|
||
VHOSTS.md. HTTPS upstream certificate verification is disabled when `tls_verify` is omitted. Set `tls_verify: true` to validate the origin certificate chain and identity. For verified IP-based origins, use a DNS `tls_server_name` present in the
|
||
certificate and provide `tls_ca_pem` when the issuer is private. Disabling verification
|
||
retains TLS encryption and all backend address/DNS policy checks. Certificate metadata references
|
||
files under `<data-dir>/certs`; it does not create a vhost or expose a listener.
|
||
First-class redirect hosts live under `redirects`; see `REDIRECTS.md` for matching, target, status, path/query and cache behavior.
|
||
|
||
ACL rules have `network` CIDR and `action` (`allow`/`deny`). Omitted ACL means no
|
||
restriction in that scope; an explicit empty ACL defaults to deny. Matching deny
|
||
wins. Host and route decisions are ANDed. Identity is the socket peer, not an
|
||
untrusted forwarded header. There are up to 128 rules per ACL.
|
||
|
||
Headers run global, then vhost (where applicable), then route rules. `set` replaces, `append` adds a
|
||
separate value and `remove` accepts no value. Transport, WebSocket handshake and
|
||
client-identity headers are protected. Up to 32 rules per direction/scope, 128-byte
|
||
names and 8192-byte values are allowed. Response rules and matching vhost security policy also cover locally generated
|
||
proxy errors/denials. Multiple Set-Cookie fields remain separate.
|
||
|
||
Routing matches exact normalized hostnames, then Nginx-style location precedence: exact path,
|
||
longest priority prefix (`^~`), first matching regex in declaration order, then longest ordinary
|
||
prefix at a segment boundary (`/api` matches `/api/x`, not `/apix`). Case-insensitive regex is
|
||
also supported. Repeated literal slashes are merged for route/ACL selection, while an unrevised
|
||
backend request retains the original path/query. Locations can explicitly strip/replace/regex-
|
||
rewrite the upstream path or return a local response. Ambiguous encoded separators and traversal
|
||
forms remain rejected. Vhost pools support load balancing as described in VHOSTS.md; locations
|
||
may pin traffic to one member. Wildcard hostname matching is supported only in the documented
|
||
leading-label form; hostname regex remains unsupported.
|
||
|
||
## Metrics and logs
|
||
|
||
Metric names use `rust_proxy_` and bounded aggregate labels. Transport counters cover
|
||
HTTP/2 requests/connections, TLS handshakes/errors and WebSocket tunnel totals,
|
||
active tunnels, errors, frame bytes and closed-tunnel duration sum/count. Ordinary
|
||
HTTP body bytes and WebSocket frame bytes are separate measurements.
|
||
|
||
Access entries are emitted at exchange completion or tunnel closure, not only when
|
||
headers arrive. WebSocket exchanges retain status 101 and a `websocket_closed`
|
||
outcome when cleanly released. Errors/cancellation affect the outcome. No query
|
||
values, request bodies, cookies, password material or WebSocket payloads are logged
|
||
by the access logger. Error diagnostics use stderr tracing. The in-memory access ring retains up to 4096
|
||
completed entries; live SSE/UI snapshots use the newest 256 by default. The dashboard can
|
||
temporarily expand an open Live Logs window to the retained 4096-entry buffer and keep
|
||
merging new live snapshots until that window closes. This is not a durable lossless log archive.
|
||
|
||
|
||
## 0.1.0 cache, listener and diagnostics additions
|
||
|
||
| Method / path | Contract |
|
||
|---|---|
|
||
| GET /assets/langs.json | Public compiled language bundle: default ID, discovered language metadata/translations |
|
||
| GET /api/cache | Authenticated cache settings, metrics and bounded object metadata; never returns cached bodies |
|
||
| POST /api/cache/purge | Full/global or vhost-scoped purge with the normal mutation protections and audit |
|
||
| POST /api/cache/invalidate | Selective authenticated PURGE/BAN by exact URL, prefix, tag, safe regex or cache key; optional host/vhost scope |
|
||
| GET /api/cache/bans | List active in-memory BAN rules |
|
||
| DELETE /api/cache/bans/{id} | Remove one BAN rule with audit |
|
||
| POST /api/cache/cold/cleanup | Start an authenticated/CSRF-protected asynchronous selective Cold Cache cleanup job |
|
||
| GET /api/cache/cold/cleanup/{id} | Read status/result of an authenticated selective cleanup job |
|
||
|
||
Configuration includes defaulted `listeners`, `cache`, `transform`, `compression`, `server_header`
|
||
objects and optional per-route `cache` (`false` bypasses, null/omitted inherits).
|
||
The complete documented field contracts are in SETTINGS.md and CACHE.md. Listener
|
||
changes persist immediately but require restart; all other settings here are hot
|
||
published. Settings forms use revisioned section writes where one settings area can
|
||
be changed independently; full-document PUT remains for changes that must publish
|
||
multiple sections atomically.
|
||
|
||
Status and SSE snapshots include `cache`, `system`, and `listeners` with active,
|
||
desired, startup-default addresses and `restart_required`. Metrics include four
|
||
compression counters; log entries include `cache` (HIT/MISS/BYPASS, or `-` before
|
||
cache classification). OS measurements are optional/null and sampled off the
|
||
request path. Unsupported is not represented as zero. Prometheus adds fixed-name
|
||
cache/resource series; absent OS measurements are omitted. No arbitrary URL/cache-key
|
||
labels are introduced and the metrics endpoint remains authenticated.
|
||
|
||
The language bundle is generated by build.rs from langs/*.json and embedded into the
|
||
binary; uploading a new file requires recompilation/restart. It exposes public
|
||
translations only, not filesystem paths, administrator data or credentials.
|
||
|
||
|
||
### Web Services
|
||
|
||
`web_services` contains reusable static-file service definitions. Use `PUT /api/config/web-services` to replace that section with revision CAS. A vhost or location selects `{ "type": "web_server", "web_service_id": "..." }`; an omitted `source` remains `{ "type": "reverse_proxy" }` for compatibility. Filesystem settings remain on the Web Service and are not duplicated inside vhosts. See [WEB_SERVER.md](WEB_SERVER.md).
|
||
|
||
## 0.2.0 vhosts and backend state
|
||
|
||
`vhosts` defaults to an empty array. All models, enum values, defaults and bounds
|
||
are documented in VHOSTS.md, with complete examples under `examples/vhosts-*.json`.
|
||
Use the same revisioned PUT endpoint; there is no unauthenticated health-control
|
||
endpoint or separate unsynchronized frontend/backend write API.
|
||
|
||
`GET /api/backends` returns `{pools, active_frontends, desired_frontends}`. Pool state
|
||
includes `id`, `algorithm`, `ready`, `health_mode`, `sticky_mode`, `keepalive`,
|
||
`members`. Members expose ID/origin/weight, status, healthy flag, active exchanges,
|
||
request/error/byte totals, EWMA byte rate/header latency, last check/result, failure
|
||
and success counters, transition count, last upstream status and upstream-client
|
||
generation/request/age/rotation counters. It does not expose affinity tokens. Member status is up/down/checking/draining/disabled. In health-off
|
||
mode, eligibility is not proof that the origin is live. Draining describes operator
|
||
intent; inspect `healthy` as well. `ready` reflects new-assignment availability;
|
||
a pool containing only draining members may still serve existing cookie sessions.
|
||
|
||
Status contains `backend_pools` and `vhosts`; listener state additionally includes
|
||
`active_endpoints`, `desired_endpoints`. Each endpoint reports its address, TLS and
|
||
H2/H3 transport capabilities. `restart_required` covers socket and listener-capability
|
||
changes. Listener capabilities are configured only under `listeners.endpoints`. A vhost
|
||
then narrows them with `http2_enabled` and `http3_enabled`; both default true. For TLS,
|
||
SNI selects either H2+HTTP/1.1 or HTTP/1.1-only ALPN for the matched vhost, so disabling
|
||
H2 on one hostname does not disable it for another hostname sharing the listener.
|
||
|
||
Metrics add `rust_proxy_backend_*` per bounded vhost/member labels. Access entries
|
||
add nullable `backend` (`vhost/member` for a forwarded pool exchange). Cache hits
|
||
have no actual upstream member request and do not increase its request counters.
|
||
The `/assets/vhosts.js` frontend uses the same API/CSRF/revision
|
||
boundaries; the static script itself, like other frontend assets, is publicly readable.
|
||
|
||
## 0.3.0 certificate upload and scoped operations
|
||
|
||
`POST /api/certificates/upload` accepts JSON
|
||
`{"cert_pem":"PEM chain","key_pem":"PEM private key","hosts":["app.example"]}`.
|
||
It requires the same session, Origin and CSRF guards as configuration updates.
|
||
The request limit is 300 KiB; each PEM value is at most 128 KiB. At most two
|
||
blocking upload workers run, and the directory holds at most 128 upload-prefixed
|
||
pairs. This endpoint is staging only, not a configuration mutation.
|
||
|
||
Success: `{"certificate":{"id":"upload-...","covered_hosts":["app.example"],
|
||
"hosts":[],"cert_file":"upload-....pem","key_file":"upload-....key"},"staged":true}`.
|
||
`covered_hosts` contains the normalized DNS names extracted from the leaf certificate
|
||
SubjectAltName extension; `hosts` remains the configured active SNI bindings. No key or
|
||
certificate PEM is returned. Add the metadata to `config.tls.certificates` and its ID
|
||
to a vhost's `certificate_id`, then save `tls`/`vhosts` as needed. The panel keeps a
|
||
combined full-document Save only when staged certificate metadata and vhost assignment
|
||
must be published atomically. Unknown certificate references, mismatched keys,
|
||
uncovered DNS names and conflicting same-name selections fail validation.
|
||
There is no ACME enrollment, expiry/chain-trust guarantee or remote PEM retrieval.
|
||
|
||
`GET /api/cache` adds `vhosts: [{id, usage: {entries, resident_bytes}, settings}]`.
|
||
`POST /api/cache/purge?vhost=app` purges only that configured vhost and invalidates
|
||
its pending fills. Missing/unknown IDs return 404, not an accidental global purge.
|
||
Omitting the query purges all entries. Both actions are audited and require CSRF;
|
||
an empty body is accepted. Configuration updates purge only when cache compatibility
|
||
changes; unrelated section saves retain both hot and cold objects.
|
||
|
||
`POST /api/cache/cold/cleanup` accepts a JSON selection with optional
|
||
`unused_for_seconds`, optional `oldest` (maximum count) and `hits_zero`. At least one
|
||
criterion is required. It returns HTTP 202 with a `ColdCleanupJob`; poll
|
||
`GET /api/cache/cold/cleanup/{id}` until `status` is `done`. The result contains
|
||
`removed_entries` and `freed_bytes`. This is intentionally separate from full/scope purge
|
||
but reuses the same internal Cold Cache candidate/removal machinery as automatic GC.
|
||
|
||
### 0.22.1 selective cache invalidation
|
||
|
||
`POST /api/cache/invalidate` accepts an `InvalidationRequest`:
|
||
|
||
```json
|
||
{
|
||
"operation": "purge",
|
||
"selector": "exact",
|
||
"value": "/assets/app.js?v=42",
|
||
"host": "app.example",
|
||
"vhost": "application",
|
||
"ttl_seconds": null,
|
||
"confirm_wide": false
|
||
}
|
||
```
|
||
|
||
`operation` is `purge` or `ban`. `selector` is `exact`, `prefix`, `tag`, `regex` or
|
||
`cache_key`. `host` and `vhost` are optional independent scope restrictions. BAN accepts
|
||
`ttl_seconds` (1..604800, default 3600) and the runtime keeps at most 1024 active rules.
|
||
Every regex operation and prefix `/` requires `confirm_wide=true`; the browser panel asks
|
||
for a danger confirmation before sending it. Regex input is length-bounded and compiled
|
||
with Rust's non-backtracking `regex` engine plus a compiled-size limit.
|
||
|
||
The response includes `result` with operation/selector/value/host/vhost,
|
||
`affected_entries`, optional `ban_id`, `elapsed_ms` and `status`, plus current cache metrics
|
||
and active BAN rules. Selective PURGE removes RAM and Cold Cache matches, removes the full
|
||
`Vary` family under a matched primary key and prevents fills that started before the PURGE
|
||
from publishing afterward. BAN creation does not synchronously scan the store: matching
|
||
objects created before the rule are rejected on lookup and removed incrementally by cache
|
||
maintenance; fresh objects created after the rule may be stored. BAN rules are runtime
|
||
state and are not persisted across process restart.
|
||
|
||
`GET /api/cache/bans` returns the active rule views. `DELETE /api/cache/bans/{id}` removes
|
||
one rule. All invalidation mutations use the standard administrator session, same-origin
|
||
request marker and CSRF protections; requested operator/scope information is written to the
|
||
existing audit log.
|
||
|
||
The `transform` configuration section is replaceable through the normal revision-CAS
|
||
`PUT /api/config/transform` endpoint. Vhost objects carry a `transform` override and
|
||
Docker assignments carry `transform_enabled`; both default to disabled per vhost.
|
||
Global transform settings also expose resource controls: `memory_budget_bytes` (1 MiB..2 GiB,
|
||
default 128 MiB), `max_body_bytes` (1 KiB..32 MiB, default 2 MiB), and `max_inflight`
|
||
(1..1024, default 32). `preset` is optional UI intent metadata with values
|
||
`conservative`, `recommended`, `performance`, or `uber`; numeric limits remain authoritative.
|
||
Resource pressure is fail-open: work that cannot reserve the configured memory/concurrency
|
||
capacity is left unchanged rather than queued behind full-body transformation work.
|
||
|
||
`GET /api/transform` returns in-memory transformation observability for the administration
|
||
Transform workspace: totals and per-vhost counters for examined/applied/skipped/error
|
||
responses, transformed cache HITs, original/transformed/saved bytes, accumulated transform
|
||
time, applied counts by content kind and bounded skip/fail-open reasons. These counters are
|
||
runtime diagnostics and reset when the RustProxy process restarts.
|
||
|
||
`GET /api/logs` accepts optional `vhost`, `q`, exact numeric `status`, `cache` and
|
||
`limit` parameters. Limit clamps to 1..4096 and defaults to 256; search length is at most
|
||
256 bytes. Filters run over the retained ring before the limit is applied. `cache` accepts
|
||
HIT, MISS, BYPASS or `-`. Entries now include nullable `vhost` independently of `backend`:
|
||
cached and static responses can be attributed without claiming that an origin was contacted.
|
||
SSE still sends the newest 256 entries per global snapshot. The Live Logs UI can seed a
|
||
temporary 4096-entry buffer on demand and then merge subsequent live snapshots into it until
|
||
the window closes. This is not a durable log stream or a per-tenant authorization boundary.
|
||
|
||
The status endpoint keeps existing field names, adds feature flags for new
|
||
capabilities, and assembles small values rather than one deeply expanded macro.
|
||
The management script `/assets/management.js` is a public static asset; APIs remain
|
||
protected. Pages and per-vhost cache fields use the standard configuration endpoint.
|
||
|
||
## 0.4.0 certificate inventory, forwarding, auth and listeners
|
||
|
||
`GET /api/certificates` requires the existing admin session and returns revision,
|
||
metadata-only certificates including `covered_hosts`, active `hosts`, `assigned_vhosts`,
|
||
file names and the per-entry default flag, plus `acme:false`. Inventory reads only the
|
||
public certificate PEM on a blocking worker and derives current DNS SubjectAltName values,
|
||
so older/manual entries and in-place renewals do not depend on stale persisted coverage
|
||
metadata. Persisted `covered_hosts` is only a fallback when the public PEM cannot be read.
|
||
It never reads a private key for this enrichment and never returns PEM contents or private-key bytes. Existing upload remains a
|
||
CSRF-protected staging operation. The inventory UI saves its returned metadata in
|
||
`config.tls.certificates`; vhost assignment is a later independent configuration
|
||
save. Every configuration mutation retains revision CAS and validation.
|
||
|
||
Vhost additions: `tinyauth`, `server_header`, `diagnostic_headers`,
|
||
`backend_header`, `backend_timing_header`, and
|
||
`frontend.listener_ids`. Global additions: `forwarding`,
|
||
`diagnostic_headers`, and `listeners.endpoints`. Each effective
|
||
TLS binding can carry `http3` with the five fields described in HTTP3.md. These
|
||
are full-config fields, not independent unaudited mutation endpoints. Unknown or
|
||
invalid references, trust ranges, URLs, limits and protected header names fail
|
||
validation before publication.
|
||
|
||
Status contains named_endpoints and active/desired endpoint transport metadata,
|
||
H3 connection counters and feature flags. Feature flags describe included source
|
||
capabilities, not evidence of successful build/conformance testing. Existing
|
||
Prometheus metrics add rust_proxy_http3_connections_total,
|
||
rust_proxy_http3_active_connections and rust_proxy_http3_rejected_connections_total
|
||
and rust_proxy_http3_requests_total. See source `metrics.rs` for exact HELP/type
|
||
contracts and INTEGRATIONS_040.md for configuration examples.
|
||
|
||
|
||
## 0.22.3 certificate hot reload and external deployment
|
||
|
||
`POST /api/certificates/reload` is an authenticated, Origin/CSRF-protected control-plane
|
||
operation. It re-reads every certificate/key file referenced by the current desired
|
||
configuration, validates a fresh immutable routing/TLS snapshot and publishes it at the
|
||
**same configuration revision**. Invalid or mismatched material returns an error and the
|
||
previous snapshot stays active. The response contains `reloaded`, `revision`, `public_tls`,
|
||
`http3_new_handshakes` and `administration_restart_required`. New public H1/H2 TLS and HTTP/3
|
||
handshakes use the refreshed certificates; established connections are not renegotiated.
|
||
Administration TLS is startup-fixed and therefore reports when a process restart is needed.
|
||
|
||
`POST /api/certificates/{certificate_id}/reload` is the targeted variant for a manual/file-backed certificate (`issuer == null`). It validates the requested configured ID and atomically republishes the immutable TLS snapshot at the same configuration revision. It is intended for external renewers that replace the files already referenced by RustProxy. Application-managed ACME certificate IDs are rejected with `409` because their renewal pipeline owns those files. The endpoint accepts either the normal administrator session + CSRF flow or a scoped Certificate API token.
|
||
|
||
`POST /api/certificates/deploy` accepts
|
||
`{"certificate_id":"shared-cert","cert_pem":"...","key_pem":"..."}` under the same
|
||
authenticated Origin/CSRF boundary. It is a convergent upsert for externally owned certificate
|
||
ids: a missing id creates a manual certificate entry, an existing manual id is rotated in place,
|
||
and an identical RustProxy-owned generation is a no-op. The pair is validated **inside the running
|
||
RustProxy process** and changed material is staged under immutable generation-specific files before
|
||
a revision-CAS configuration publish. This is important for root-run Certbot hooks when RustProxy
|
||
itself runs as an unprivileged service account: final generations are created with the service's
|
||
filesystem identity rather than `root:root`. Managed ACME ids are rejected, and failed
|
||
configuration publication removes any newly staged generation.
|
||
|
||
For a certificate owned by a local external issuer such as system Certbot, prefer the local
|
||
CLI deploy hook, which calls that loopback API after bounded local file checks:
|
||
|
||
```bash
|
||
/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy tls deploy --id shared-cert \
|
||
--cert /etc/letsencrypt/live/example/fullchain.pem \
|
||
--key /etc/letsencrypt/live/example/privkey.pem
|
||
```
|
||
|
||
The command resolves Certbot's local `live/` symlinks, checks bounded regular files/private-key permissions, then sends the pair to `POST /api/certificates/deploy`. The running service validates the certificate/key and DNS SANs, stages generation-specific files inside RustProxy's private certificate directory, and creates or switches the **manual** certificate id through the normal revision-CAS configuration path. A single id may already be selected by multiple vhosts, so those vhosts switch together
|
||
without individual edits. RustProxy-managed ACME entries (`issuer != null`) are rejected to
|
||
avoid two renewal owners for the same lineage. The existing `/api/certificates/upload` remains
|
||
the generic PEM staging endpoint; private certificate paths are never accepted from a remote
|
||
request.
|
||
|
||
## 0.23.0 scoped Certificate API credentials
|
||
|
||
External automation no longer needs a full administrator browser session. From **Settings -> Certificate API**, an administrator can create multiple dedicated credentials (up to 32), each consisting of a Key ID, Bearer token and optional non-secret comment. Each Key ID can be rotated independently. The clear token is returned only by the create/rotation response; SQLite stores only its SHA-256 hash plus metadata/comment. Every credential has exactly two scopes: certificate deploy and certificate reload.
|
||
|
||
API clients send both headers:
|
||
|
||
```text
|
||
Authorization: Bearer <token>
|
||
X-RustProxy-Certificate-Key: <key-id>
|
||
```
|
||
|
||
A Certificate API credential is accepted only by `POST /api/certificates/deploy`, `POST /api/certificates/reload`, and `POST /api/certificates/{certificate_id}/reload`; it cannot read configuration, logs, metrics or any other administration endpoint. Non-loopback use requires the configured HTTPS administration surface. The normal administrator session + Origin/request-marker + CSRF path remains valid for the same operations. Credential management remains administrator-only: `GET /api/certificate-api` lists metadata, `PUT /api/certificate-api` creates/rotates one Key ID, `DELETE /api/certificate-api/{key_id}` revokes one token, and `DELETE /api/certificate-api` revokes all tokens.
|
||
|
||
Example external deploy:
|
||
|
||
```bash
|
||
curl --fail --show-error \
|
||
-H 'Authorization: Bearer TOKEN' \
|
||
-H 'X-RustProxy-Certificate-Key: certbot-main' \
|
||
-H 'Content-Type: application/json' \
|
||
--data-binary @deploy.json \
|
||
https://proxy-admin.example/api/certificates/deploy
|
||
```
|
||
|
||
For an on-host Certbot hook, a token is optional: invoke the installed binary directly instead:
|
||
|
||
```bash
|
||
/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy tls deploy --id shared-cert \
|
||
--cert /etc/letsencrypt/live/example/fullchain.pem \
|
||
--key /etc/letsencrypt/live/example/privkey.pem
|
||
```
|
||
|
||
`dev.sh` is development tooling and is not required for production certificate rotation.
|
||
|
||
## 0.5.0 cache inspector and certificate jobs
|
||
|
||
`GET /api/cache` additionally returns `dashboard: {domains, objects,
|
||
matching_objects, offset, limit, statistics_revision}`. Domain rows flatten
|
||
hits/misses/bypasses/stored/saved_body_bytes/reasons beside vhost/domain and
|
||
resident counts. Object rows contain vhost/domain/path/body_bytes/resident_bytes/
|
||
hits/age_seconds/remaining_ttl_seconds, never bodies, request headers or queries.
|
||
`vhost=ID` filters, `vhost=` selects legacy only; omitted selects all. Limit 1..200
|
||
(default 50), offset <=16384. Unknown vhost ->404. The same scope semantics apply
|
||
to POST /api/cache/purge. Existing global metrics remain process-lifetime values.
|
||
|
||
The historical 0.5 `GET /api/acme` response did not support renewal; see the
|
||
current 0.7 response below.
|
||
`POST /api/acme/issue` accepts at most 16 KiB of JSON with certificate_id, email,
|
||
domains, environment, agree_tos, confirm_production, dns_server, dns_port, key_name,
|
||
key_secret, key_algorithm, sign_query and propagation_seconds. See
|
||
ACME_RFC2136.md for exact constraints. It returns 202 `{job}` only after admission;
|
||
one process-wide job, 429 when busy. Job fields: id, certificate_id, status,
|
||
environment, message, certificate. The ready certificate is immutable PEM metadata,
|
||
not private bytes. Import is a separate normal config CAS write. Both endpoints
|
||
are behind the administrator session; POST retains Origin/CSRF and audit.
|
||
|
||
LogEntry adds `cache_reason`, a fixed string token. Debug response headers add
|
||
X-Cache-Reason when X-Cache is enabled, and standardized RFC 9211 `Cache-Status` detail
|
||
when enabled. Transformed cached representations add the `transformed` Cache-Status
|
||
extension parameter. `X-Cache-Status` is stripped if supplied by an origin but is not
|
||
emitted by RustProxy.
|
||
`diagnostic_headers.hide_mock_origin` is removed and old documents containing it are rejected on
|
||
export; `request_id` is the only diagnostic-header switch. The generated request ID is a 64-hex-character SHA-256 value and the same protected value is forwarded to the origin as `X-Request-ID`. No revision response-header behavior remains. TLS listener
|
||
http3 omission is default-on; explicit null is persisted off. Legacy listeners use
|
||
https_http3 with identical default/null behavior.
|
||
|
||
## 0.6 provider and scrape additions
|
||
|
||
`POST /api/acme/issue` accepts `provider`: `http01`, `cloudflare`, `ovh` or
|
||
`rfc2136` (omitted retains RFC2136 compatibility). Common fields include
|
||
`certificate_id`, `domains`, `email`, `environment`, `agree_tos` and
|
||
`confirm_production`. DNS providers also accept `propagation_seconds`.
|
||
Cross-provider/unknown fields are rejected. Provider-specific fields, secret
|
||
handling and request examples are in ACME_PROVIDERS.md. `GET /api/acme` returns
|
||
only public job status and validated certificate metadata, not input credentials.
|
||
Issuer metadata on certificates is optional `{ "provider": "cloudflare",
|
||
"environment": "staging" }`. Do not confuse metadata with CA trust validation.
|
||
|
||
`config.prometheus` controls the new dedicated HTTP scrape endpoint, which uses
|
||
actual-peer CIDRs and optional bearer authentication, not administrator cookies.
|
||
The exact configured GET path only is accepted (query strings rejected). Disabled
|
||
or changed/restart-pending settings return 404; denied peers 403; invalid tokens
|
||
401; non-GET 405; exhausted scrape capacity 429. No administrative actions are
|
||
available there. Admin `/api/metrics` and `/metrics` remain session-protected
|
||
previews on the admin listener, even when the dedicated endpoint is disabled.
|
||
See PROMETHEUS.md for the full settings and metric contract. Traffic analytics is an independent collector group (`include_traffic`) that exports bounded country, fixed User-Agent/method/source, status/cache and configured-route aggregates without raw IP/path/full-UA labels. Docker discovery is an independent collector group (`include_docker_vhosts`) and exports only dedicated `rust_proxy_docker_vhost_*` / `rust_proxy_docker_backend_*` families; ordinary vhost/backend families exclude dynamic Docker routes.
|
||
|
||
## 0.7 renewal and host access
|
||
|
||
`GET /api/acme` now returns `{job, providers, automatic_renewal:true,
|
||
interval_seconds:43200, renewals}`. `renewals` is keyed by certificate ID and
|
||
contains last_checked, next_check, last_renewed, not_after (Unix seconds), failures,
|
||
status and a public message token. No credential/account data is returned.
|
||
`GET /api/certificates` also includes renewal observations. Statuses are observations,
|
||
not a promise of a successful future CA operation.
|
||
|
||
`POST /api/acme/renew` accepts only `{certificate_id, dry_run:false}`. It uses an
|
||
existing managed lineage and returns 202 `{job}` after admission, or 404 unknown ID,
|
||
409 unmanaged/invalid profile or disabled prerequisites, 429 worker busy. Actual
|
||
worker/CA failures arrive in job state. No force-renew flag is offered. Dry runs
|
||
never deploy their test certificate and never postpone a due real renewal.
|
||
The usual session, Origin/CSRF protections and audit apply.
|
||
|
||
A certificate's `issuer` supports `auto_renew` (default true for saved managed
|
||
metadata, explicit false opts out). Toggle through ordinary revisioned config;
|
||
no provider credentials need resubmitting. Old immutable generations are retained.
|
||
|
||
`POST /api/basic-auth/hash` accepts `{username, password}` with at most 4096 bytes
|
||
of request JSON. It requires an administrator session and the same mutation/CSRF
|
||
guards, returns `{username, password_hash}`, and never saves the plaintext password.
|
||
Passwords are 12..128 UTF-8 bytes with no control characters. Usernames are 1..128
|
||
bytes, no colon/control characters. Hashing has two shared blocking-worker slots;
|
||
no available slot returns 429. Invalid input returns 400. This is NOT a public
|
||
login endpoint and does not create a browser session for a vhost.
|
||
|
||
A vhost accepts `websockets` and `basic_auth` with enabled, realm, require_tls,
|
||
forward_credentials and users[{username,password_hash}]. Basic and Tinyauth cannot
|
||
be enabled simultaneously. Hash import accepts only the bounded Argon2id policy
|
||
created by the hash endpoint, not arbitrary-cost PHC strings or htpasswd formats.
|
||
Missing `websockets` retains the previous true behavior; the new-host UI selects
|
||
false. Basic TLS defaults true and forwarding defaults false.
|
||
|
||
Backend pool members have `pool_eligible` (default true). False excludes that
|
||
member from default load balancing and sticky selection without blocking an
|
||
explicit location `backend_id`. Visual custom locations edit existing vhost
|
||
`routes`: ID, path_prefix, backend_id, enabled, cache, ACL and advanced headers.
|
||
This is prefix routing, not regex, rewrite or nginx configuration evaluation.
|
||
See [host access](VHOST_ACCESS.md) for cumulative ACL and authentication semantics.
|
||
|
||
|
||
## 0.18 Docker discovery additions
|
||
|
||
| Method / path | Contract |
|
||
|---|---|
|
||
| `GET /api/docker` | Authenticated cached in-memory Docker-agent observations; no agent poll and no configuration write |
|
||
| `POST /api/docker/refresh` | Authenticated + Origin/CSRF immediate poll of enabled agents followed by ephemeral route reconciliation; no configuration revision write |
|
||
| `PUT /api/config/docker-agents` | Revision-CAS replacement of desired Docker-agent settings, per-agent domains and service hostname/alias/IP assignments |
|
||
|
||
`config.docker_agents` stores desired agent endpoint/token/base-domain/IP override/listener references, a per-agent 5..3600 second inventory poll interval, and assignments. Inventory data returned by agents is not persisted in the configuration document. The normal configuration API remains authoritative for desired state; the Docker observation API is operational state only. Docker assignments may also carry an ACL profile, inline ACL, health-check settings and per-vhost proxy timeouts. Publishing/unpublishing is therefore an ordinary CAS configuration update and does not require polling the agent.
|
||
|
||
## HTTP/2 lifecycle settings
|
||
|
||
As of 0.24.3 the preferred configuration is per listener: `listeners.endpoints[].binding.http2_settings` contains `idle_timeout_seconds`, `max_connection_lifetime_seconds` and `drain_timeout_seconds` for that TLS H2 or cleartext H2C endpoint. Update these values by revision-CAS replacement of the `listeners` section (`PUT /api/config/listeners`), the same ownership model used by per-listener HTTP/3 limits. `max_connection_lifetime_seconds: 0` disables the absolute lifetime limit. Idle and lifetime expiry both use graceful GOAWAY/drain shutdown.
|
||
|
||
The top-level `http2` object introduced in 0.24.0 and `PUT /api/config/http2` remain accepted for backward compatibility. They are a fallback only for listeners that do not yet have explicit `binding.http2_settings`; the structured 0.24.3 UI writes listener-scoped values.
|
||
|
||
The runtime system diagnostics object includes `platform`, supplied by the shared system-statistics backend (`Linux`, `FreeBSD`, `OpenBSD`, `NetBSD`, or the generic/unsupported fallback). The browser only renders that value; it does not detect the operating system itself.
|