network:proxy service¶
Publishes a module through Caddy — the module's public face. It owns the hostname, the certificate, the upstream handler and the firewall alias that decides who may reach it. Nothing here touches a guest; the changes land on the proxy and the firewall, and the workload never notices.
10 fields — all in-place, all apply: "reconcile".
The module's public face. Nothing here touches a guest: the changes land on Caddy and the firewall, and the workload never notices. The largest drift blind spot — see recommendation 1.
Why reconcile and not set¶
update-service.sh rewrites this module's whole Caddy site block and its OPNsense alias from the declared values on every pass, then reloads. That is already idempotent and already handles removal — a handler that should no longer exist is deleted, which no scalar field diff can express. Flattening it into set fields would lose that. The manifest declares the class, which is what makes --set sanctioned; the apply stays where the domain knowledge is.
Because there is no report-service.sh, module-manager module drift reports these fields as not-reported rather than comparing them — the converge is trusted to have made config true. That is the blind spot recommendation 1 closes.
Fields¶
network:proxy owns 9 declared field(s). Each table below carries the field's full definition and, where the service applies it, its ADR-020 change semantics.
proxyDomain¶
Public domain name for the reverse proxy. Caddy obtains a TLS certificate for this domain.
| Attribute | Value |
|---|---|
| Type | string |
| Default | <vmname>.<tappaas.domain> |
| Format | ^[a-zA-Z0-9](https://codeberg.org/TAPPaaS/TAPPaaS/src/branch/main/src/foundation/network/services/proxy/%5Ba-zA-Z0-9.-%5D%2A%5Ba-zA-Z0-9%5D)?$ |
| Example | vaultwarden.test.tapaas.org |
| Required by | (none) |
| Used by | network:proxy |
| Change class | in-place |
| Apply mode | reconcile |
About the field. If not set, defaults to
Why this change class. The public hostname. Changing it re-issues the ACME certificate and moves the handler; the module itself is untouched, so no guest downtime — though clients on the old name stop resolving as soon as DNS follows.
proxyPort¶
Target port on the module VM that the reverse proxy forwards traffic to
| Attribute | Value |
|---|---|
| Type | integer |
| Default | 80 |
| Minimum | 1 |
| Maximum | 65535 |
| Example | 8080 |
| Required by | (none) |
| Used by | network:proxy |
| Change class | in-place |
| Apply mode | reconcile |
| Normalizer | integer |
About the field. The port the service listens on inside the VM
Why this change class. The upstream port Caddy forwards to. A handler rewrite, applied live.
proxyUpstreamTls¶
Reverse-proxy to an HTTPS upstream instead of plain HTTP. Set true for backends that only speak TLS — e.g. the OPNsense GUI on :8443. network:proxy renders the Caddy upstream as https:// with upstream certificate verification skipped (internal/self-signed backends).
| Attribute | Value |
|---|---|
| Type | string |
| Default | false |
| Allowed values | true — Upstream is HTTPS (skip upstream cert verification)false — Upstream is plain HTTP (default) |
| Required by | (none) |
| Used by | network:proxy |
| Change class | in-place |
| Apply mode | reconcile |
| Normalizer | boolean |
Why this change class. Whether the hop from Caddy to the module is itself TLS. Declared as a string in module-fields.json, so the boolean normalizer is what makes 'true' and true one value.
proxyUpstreamHttp1¶
Force HTTP/1.1 to the upstream (os-caddy HttpVersion=http1). Required for apps whose UI rides a WebSocket behind a TLS upstream — e.g. the UniFi OS console. Without it, Caddy negotiates HTTP/2 with the upstream, which cannot carry a WebSocket Upgrade and returns 500, so the SPA renders blank.
| Attribute | Value |
|---|---|
| Type | string |
| Default | false |
| Allowed values | true — Force HTTP/1.1 to the upstream (WebSocket support)false — Default upstream HTTP versions (HTTP/1.1 + HTTP/2) |
| Required by | (none) |
| Used by | network:proxy |
| Change class | in-place |
| Apply mode | reconcile |
| Normalizer | boolean |
Why this change class. Force HTTP/1.1 upstream, for a backend that cannot speak h2c.
proxyPreserveHost¶
Force the upstream Host header to the public domain (Caddy header_up Host
| Attribute | Value |
|---|---|
| Type | string |
| Default | false |
| Allowed values | true — Send Host: false — Caddy default upstream Host |
| Required by | (none) |
| Used by | network:proxy |
| Change class | in-place |
| Apply mode | reconcile |
| Normalizer | boolean |
Why this change class. Pass the original Host header through, for a backend that generates absolute URLs from it.
proxyTls¶
How network:proxy obtains the public TLS certificate for this domain. OMIT IT to inherit the environment's domains.dnsMode (config/environments/
| Attribute | Value |
|---|---|
| Type | string |
| Allowed values | dns01 — Wildcard certificate via os-acme-client; bound by refid through Caddy CustomCertificate (no per-module ACME, whttp01 — Per-domain ACME HTTP-01 via Caddy itself (no DNS-API needed, but the domain must be reachable from the interne |
| Required by | (none) |
| Used by | network:proxy |
| Change class | in-place |
| Apply mode | reconcile |
Why this change class. Which certificate strategy serves the domain (per-service or the environment wildcard).
proxyAllowedZones¶
Zones (and the literal 'internet') permitted to reach this service through the reverse proxy — or 'none' (no one, except through proxyAccess path rules), or networks named directly, as a.b.c.d/n or x:y::/n: a provider's published caller range, for an endpoint one cloud service must reach without opening it to the whole internet. A malformed network is refused, like an unknown zone. network:proxy compiles this into an os-caddy access list (allow-list by client subnet) attached to the handler; non-matching clients get HTTP 403. On a site with IPv6, each zone's IPv6 network is allowed next to its IPv4 one. Under ADR-021 D2 this is the ONLY place a zone restricts a published service: split-horizon DNS returns the same address (the DMZ gateway) to every caller, so the answer encodes no entitlement and the access list is what narrows it.
| Attribute | Value |
|---|---|
| Type | array |
| Default | <internal default: every Active 'Service' zone plus home, work and mgmt — NOT the internet> |
| Example | mgmt, home, 52.112.0.0/14 |
| Required by | (none) |
| Used by | network:proxy |
| Change class | in-place |
| Apply mode | reconcile |
About the field. Zero-trust by default: when omitted, a service is reachable only from the internal trusted zones, never the internet. Add 'internet' to publish it publicly (no restriction). Zone names are resolved to subnets via zones.json. Changing this re-applies on the next install/update of the module. TWO values carry real consequence and the rest is belt-and-braces: leaving it UNSET is the normal, correct configuration for a published service, and adding 'internet' is what makes it publicly reachable. Narrowing to a specific list of internal zones filters IN FRONT OF the identity gate, it is not the mechanism protecting the service (ADR-021 Case ⅓) — a zone list that lets someone through still leaves them at an Authentik login, and one that shuts them out only saves them the round trip. Do not use it to separate two populations who are each entitled to a different environment: that binds entitlement to network position, which breaks the moment a legitimate user connects from another zone (the same person on a laptop in home, a phone on guest wifi and the netbird overlay is ONE identity in three zones). Authentik group membership is where that belongs.
Why this change class. Which zones may reach the published name — the difference between an internal service and one exposed to the internet. A live firewall/Caddy change, and the field most worth being able to set through a verb rather than by hand.
proxyAccess¶
Path exceptions to the route's proxyAllowedZones: a list of rules {paths, zones, identityGate}. A request whose path matches a rule gets that rule's zones; every other path keeps the route's. paths are Caddy path matchers starting with / (a trailing * is a prefix match); zones takes the same values as proxyAllowedZones. identityGate false serves the rule's paths without the Authentik gate, for callers that cannot hold a credential at all — a webhook signed in its payload — and then needs a description saying who calls and why (a machine caller that can send a credential uses a service account through the gate instead, ADR-032 D1). Default: as the route. On a gated route Caddy removes the caller's Authorization header before the app. Allowed on the module (primary route) and on each proxyRoutes entry. With proxyAllowedZones ['none'] a route serves only its rules' paths.
| Attribute | Value |
|---|---|
| Type | array |
| Default | (none) |
| Example | [{"paths": ["/api/webhook/*"], "zones": ["internet"], "identityGate": false, "description": "Home Assistant Cloud webhooks: authenticated by the secret ID in the URL"}] |
| Required by | (none) |
| Used by | network:proxy |
| Change class | in-place |
| Apply mode | reconcile |
About the field. Reachable is not authorized: a public path is protected only by the application (a secret ID in the URL, a signed request). The longest matching path wins. A path of / or /* is refused (set proxyAllowedZones instead), as is the same path in two rules.
Why this change class. Reconciled with the route: one Caddy handle per path, before the catch-all, each with its rule's access list; handles and lists no longer declared are pruned.
proxyRoutes¶
Additional reverse-proxy routes for a VM that serves several endpoints on different ports. Each entry publishes
| Attribute | Value |
|---|---|
| Type | array |
| Default | (none) |
| Example | [{"name": "admin", "port": 9090}, {"name": "hooks", "port": 5678, "proxyAllowedZones": ["internet"]}] |
| Required by | (none) |
| Used by | network:proxy |
| Change class | in-place |
| Apply mode | reconcile |
About the field. Each entry is { name:
Why this change class. The extra hostnames a single VM publishes. Adding or removing an entry creates or prunes that route live; the primary route and the module itself are untouched.