Skip to content

Productify Proxy ​

The Productify Proxy is the front door of a Productify installation. It is a standard Caddy 2.11 web server compiled with two extra module sets: caddy-security for OAuth2/OIDC login, and the productify modules from the proxy repository. The productify modules validate credentials against the Productify Manager, work out which tenant a request is for, add identity headers before the request reaches your application, and record per-application access logs and metrics.

You normally do not write the Caddyfile by hand

The Manager renders one virtual-host block per application domain from its own data and pushes the whole Caddyfile to the running proxy through Caddy's admin API (POST :2019/load). The install-level part of the file (global options, the login portal, the Manager's own vhost) is rendered from install.yaml by pfy platform render and handed to the Manager as proxy_prelude_file. The Caddyfile examples in this section show the shape the renderer produces, so you can read and debug it. See Generated Artifacts and the Proxy Vhost Sync Loop. pfy proxy status, diff and sync inspect and drive that loop (CLI reference).

What the proxy does with a request ​

For a request to an application domain, the proxy runs these steps in this order:

  1. productify_sanitize deletes every X-User-*, X-Tenant-*, X-Auth-Type, X-Token-* and X-Proxy-Auth header from the incoming request. Only the proxy may set them.
  2. The caller is authenticated in one of three ways (table below).
  3. The tenant is resolved from the signed per-application cookie on a shared host, or from the tenant <uuid> pin on a host dedicated to one tenant.
  4. Identity headers (X-User-ID, X-Tenant-ID, ...) are added and, for HTML responses, a window.__PRODUCTIFY__ object.
  5. One access-log line and Prometheus metrics are recorded for the application.
  6. The request is forwarded to the application's upstream.

The compound directive productify_protect <app_id> with <policy> emits steps 1 to 5 in this fixed order; the rendered Caddyfile uses it for every application route. Application Routing explains each step.

Three ways to authenticate ​

CallerCredentialRoutesManager unreachable
Person in a browserOAuth2/OIDC login through the caddy-security portal under /auth/*Application routesThe request continues with only the portal's X-Token-* headers; no X-User-* headers are added (see below)
API client, personal tokenAuthorization: Bearer <PAT>Application routes503 Authentication service unavailable (fails closed)
Machine user (ESB)Authorization: Bearer <token> or Authorization: Basic <base64>/esb/* only503 Authentication service unavailable (fails closed)

Other outcomes:

  • An unauthenticated browser request is redirected (302) to /auth/oauth2/<realm>; the portal then redirects to the identity provider.
  • A PAT the Manager reports as invalid falls through to the OAuth redirect. A valid PAT whose owner has no access to this application gets 403 Forbidden: no access to this application.
  • A browser user the Manager knows but does not authorise for this application gets 403 Forbidden: not authorized for this application.
  • On /esb/*, a missing Authorization header is 401; a credential that is valid but not a machine user is 403 ESB access requires Machine User authentication. Personal tokens and OAuth sessions are therefore rejected on ESB routes.
  • Basic credentials on an application route are ignored by the PAT step and fall through to the OAuth redirect.

Architecture ​

┌──────────────┐
│   Clients    │  browsers, API clients, ESB systems
└──────┬───────┘
       │ HTTP(S) :80 / :443
       ▼
┌─────────────────────────────────────────────┐
│          Productify Proxy (Caddy)           │
│  caddy-security  – OAuth2/OIDC portal       │
│  productify_*    – credential validation,   │
│                    tenant, headers, metrics │
│  :2019 admin API – receives rendered config │
│  :2112           – /metrics /healthz /readyz│
└──────┬──────────────────────────┬───────────┘
       │ X-Proxy-Auth             │
       ▼                          ▼
┌───────────────────────┐   ┌───────────────────────┐
│  Productify Manager   │   │  Application upstreams │
│  validate-proxy-auth  │   │  (one vhost per app    │
│  validate-oauth-user  │   │   domain)              │
│  frontend-config      │   └───────────────────────┘
│  tenant catalog       │
│  wake / ready         │
└───────────────────────┘

Modules in the custom build ​

NameKindPurpose
productify (global block)Caddy appManager URL, shared secret, cache TTLs, metrics address, activator defaults
productify_protectRoute directiveExpands to the full protection chain below, in a fixed order
productify_sanitizeRoute directiveDeletes client-supplied identity headers
productify_authRoute directiveValidates a personal access token (Bearer) with the Manager
productify_esb_authRoute directiveValidates machine-user credentials (Bearer or Basic) with the Manager; ESB routes only
productify_before_authRoute directiveCounts users entering the login flow (pfy_authentication_awaiting_users)
productify_tenant_selectorRoute directiveServes /_tenant, manages the signed tenant cookie, enforces the tenant pin
productify_oauth_validationRoute directiveMaps a portal session (X-Token-Subject) to a Productify user and tenant via the Manager
productify_html_injectRoute directiveAdds window.__PRODUCTIFY__ to HTML responses
productify (route directive)Route directiveAccess log line and Prometheus metrics for one application
activator (sub-block of protect)HandlerScale-from-zero: wakes a stopped deployment through the Manager and holds or parks the request
caddy-securityThird-partyOAuth2/OIDC portal and authorization policies
caddy-dns/cloudflareThird-partyDNS-01 challenge so wildcard certificates (*.apps.example.com) can be issued

Run caddy list-modules | grep productify on the built binary to confirm the modules are present.

How the proxy talks to the Manager ​

nginx
{
  productify {
    manager http://172.17.0.1:8080
    token {env.PFY_MANAGER_TOKEN}
  }
}

token is the shared proxy secret. The proxy sends it in the X-Proxy-Auth request header on every call to the Manager API (not as an Authorization bearer token). The Manager compares it in constant time before it trusts any identity the proxy forwards. Set the same value on the Manager as PFY_PROXY_SECRET. Read it from the environment as shown; the proxy expands {env.VAR} when it starts.

The proxy refuses to start when token is empty or manager is not an http:// or https:// URL with a host. It also refuses a config whose min_proxy_version is newer than the plugin version it implements (currently 1.1.0), so a Manager that renders for a newer proxy fails loudly instead of misrouting.

Example: one application vhost ​

This is the shape the Manager renders for an application (the global block is on the Caddyfile page). vars app_id is the application's ID in the Manager.

nginx
http://app.example.com {
  vars app_id 64e163af-2f6b-4b2c-9a84-ec3c8dfbbe67

  @auth path /auth/*
  @esb path /esb/*

  # Login portal. Counts users waiting on the login page.
  route @auth {
    productify_sanitize
    productify_before_auth with {vars.app_id}
    authenticate with pocketportal
  }

  # ESB: machine users only. /esb/x becomes /esb/<app_id>/x on the Manager.
  route @esb {
    productify_sanitize
    productify_esb_auth with {vars.app_id}
    productify with {vars.app_id}
    uri strip_prefix /esb
    reverse_proxy manager:8080 {
      rewrite /esb/{vars.app_id}{uri}
    }
  }

  # Everything else: PAT or browser login, tenant, headers, metrics.
  route /* {
    productify_protect {vars.app_id} with pocketpolicy
    reverse_proxy backend:8080
  }
}

The rendered file also adds a request_header -X-... snippet on every route as a second, independent strip of identity headers, and further routes for pilet feeds, pilet bundles, language packs and comms. Application Routing lists them.

Requirements ​

  • The custom Caddy build: the published image ghcr.io/productifyfw/proxy:1.1.0 (Caddy 2.11.4, public), or a build from the proxy repository's Dockerfile or go build in caddy/ (Go 1.26).
  • A running Productify Manager, reachable from the proxy, with PFY_PROXY_SECRET set to the proxy's token.
  • An OAuth2/OIDC identity provider (the repository's docker-compose.yml ships Pocket ID).
  • DNS for every application domain pointing at the proxy.

Next steps ​

  1. Quick Start: run the proxy locally with one protected application.
  2. Caddyfile Reference: every global option and directive.
  3. Application Routing: routes, tenant resolution, headers your application receives.
  4. Monitoring: access logs, metrics, health probes.
  5. Proxy Deployment: production deployment.