Files
RustProxy/docs/API.md
T
2026-10-11 22:55:22 +02:00

43 KiB
Raw Blame History

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.

Public-vhost HTTP cache invalidation (opt-in)

PURGE /path?query and BAN /prefix operate on the normal public vhost listener and do not use an admin session. They are independently denied by default unless the administrator explicitly populates the cache's http_invalidation_allowed_cidrs allowlist in Settings → Cache. Authorization uses the existing trusted client-IP forwarding logic, vhost/host ACL and rate limits. BAN is a lazy prefix rule; PURGE evicts an exact URL. Both are restricted to the request scheme (http vs https), Host and matched vhost. A shared two-worker maintenance guard also bounds administrator-driven invalidation; additional requests receive 429. Ambiguous duplicate control headers and request bodies are rejected. See CACHE for request examples, scoping, confirmation and reverse-proxy requirements, and 0.31.0 security review for audit findings. The authenticated /api/cache/invalidate endpoint remains unchanged.

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.

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, or automatically derived from the validated administration.public_origin, 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

GET /api/status?view=live returns only volatile telemetry suitable for SSE fallback, without the heavy full configuration/OS/runtime objects. GET /api/system-info returns cached system diagnostics, host/runtime metadata and memory accounting under system, runtime and memory_accounting. Both are protected by the same cookie/origin admin guard as full status.

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 every 1/2/5/10 seconds per authenticated client (via ?interval=, default 2) and rechecks session validity. It does not queue an unlimited per-viewer event log or guarantee replay. The browser falls back to compact status/log polling only after SSE fails and stops polling after its recovery. Invalid SSE intervals are rejected with 422. An active dashboard renews idle activity, not the absolute session deadline.

Configuration history, introduced in SQLite schema 8 and retained in current schema 9, keeps the newest 512 successful manual/configuration revisions. 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.

{
  "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. Warnings and errors use the configured error file, or stderr when that file is disabled/unavailable. 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.

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:

{
  "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 administrator-session-only, Origin/CSRF-protected control-plane operation. Certificate API bearer tokens are intentionally not accepted by this global endpoint. 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. A manual entry may use simple filenames from <data-dir>/certs or two absolute external paths such as /etc/letsencrypt/live/example/fullchain.pem and /etc/letsencrypt/live/example/privkey.pem. Absolute paths are resolved again on every reload, so Certbot's live/ symlinks follow the newly renewed generation. 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 Certificate API token whose grant explicitly contains that certificate ID.

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:

/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; deploy/upload request bodies never accept host filesystem paths. Absolute paths may only be saved explicitly in administrator TLS configuration as a manual certificate source, where they are validated and never returned as PEM contents.

0.28.2 least-privilege Certificate API credentials

External automation does not need 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, optional non-secret comment and an explicit allowlist of 1-32 certificate IDs. New credentials are reload-only by default. Deploy is an independent opt-in permission.

The clear token is returned only by the create/rotation response; SQLite stores only its SHA-256 hash plus metadata and the grant. Credentials created before 0.28.2 migrate to a reload-only compatibility grant for all certificate IDs (*). Rotate those credentials after upgrading to replace the wildcard with explicit IDs.

API clients send both headers:

Authorization: Bearer <token>
X-RustProxy-Certificate-Key: <key-id>

A reload-only token may call only the targeted endpoint for an allowlisted certificate:

curl --fail --show-error -X POST \
  -H 'Authorization: Bearer TOKEN' \
  -H 'X-RustProxy-Certificate-Key: certbot-main' \
  -H 'Content-Type: application/json' \
  --data '{}' \
  https://proxy-admin.example/api/certificates/shared-cert/reload

Certificate API tokens are not accepted by the global POST /api/certificates/reload endpoint. This prevents a compromised renewal token from forcing reads/reloads of unrelated certificate material. POST /api/certificates/deploy is available only when the credential was explicitly created with deploy permission, and the certificate_id in the request body must also be present in that credential's allowlist.

A Certificate API credential cannot read configuration, logs, metrics, users 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 certificate operations. Credential management remains administrator-only: GET /api/certificate-api lists metadata/grants, PUT /api/certificate-api creates or rotates one Key ID, DELETE /api/certificate-api/{key_id} revokes one token, and DELETE /api/certificate-api revokes all tokens.

Example external deploy for a token that has Also allow deploy enabled and contains shared-cert in its allowlist:

curl --fail --show-error \
  -H 'Authorization: Bearer TOKEN' \
  -H 'X-RustProxy-Certificate-Key: deploy-hook' \
  -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:

/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 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.

Per-vhost User-Agent rate limiting (0.31.7)

The revisioned /api/config vhost rate-limit object accepts optional user_agent_limits, up to 64 entries with pattern (1..256 bytes, case-insensitive substring or * wildcard), requests (1..1000000), window_seconds (1..3600) and burst (0..1000000). The first matching rule replaces the standard and country rate for that request and uses a separate fixed-window counter for each (vhost, rule index, client IP). Requests with no matching User-Agent use the existing limiter. A User-Agent is trivially spoofable; these overrides are operational shaping, not access control. Global and vhost User-Agent blocklists continue to deny matching requests before rate limiting.