Files
member-console/docs/production-deployment.md
T
cgalo5758 0b28a9dc29 Remediate security audit findings
- Replace gorilla/csrf with net/http CrossOriginProtection
- Require valkey-password and add TLS options for session store
- End session at /logout and revoke refresh tokens
- Re-derive identity and roles from provider every five minutes
- Process each Stripe webhook event in its own Temporal workflow
- Give each outbox entry its own workflow with Temporal retries
- Guard against stale Stripe events with provider timestamps
- Derive transport security from base-url scheme
2026-09-09 13:25:43 -05:00

205 lines
11 KiB
Markdown

---
title: "Production Deployment"
audience: [admin]
summary: "The sequenced path from a built image to a running production instance: what order to configure things in, and the glue between the per-service guides that no single one of them covers."
---
# Production Deployment
This guide sequences the existing per-service guides into one path and adds
the connective pieces none of them owns on their own: the reverse proxy and
trusted-origin contract, matching the identity provider's branding to the
console's configured name, the Stripe webhook flow, and what happens the
first time the process boots. It links out to each per-service guide instead
of repeating it — read this first to know what order to do things in and
where each step's real instructions live.
> **Status.** member-console is pre-production software (see the root
> [README](../README.md)). This guide gets a self-hoster to a working
> instance; it does not claim a hardened, one-command production rollout.
> Expect to adapt each step to your own infrastructure.
## 1. Build the image and prepare secrets
Follow [hosting.md](hosting.md) to build the container image for your
registry and generate the `csrf-secret`. Every other secret this instance
needs — the OIDC client secret, the Stripe API key and webhook secret, the
FedWiki admin token, the Discourse API key — is generated or obtained in the
later steps below and injected the same way: either as an `MC_*` environment
variable or as a file referenced by that key's `-file` variant (`MC_*_FILE`),
per [environment-reference.md](environment-reference.md). Prefer the `-file`
form with your platform's secret-mounting mechanism (Docker/Podman secrets,
a Kubernetes `Secret` volume) over putting secret values directly in the
environment.
## 2. Decide the deployment shape
Read [deployment-architecture.md](deployment-architecture.md) for how
member-console fits alongside a public homepage and an identity provider —
it is infrastructure behind your own front door, not the front door itself.
Decide now which services member-console needs for your offering:
- **PostgreSQL** and **Valkey** — always required.
- **An OIDC identity provider** — always required (any OIDC-compliant
provider; see step 4).
- **Temporal** — required for provisioning, integration sync, and billing
sweeps to actually run; the console starts without it but logs
`Temporal not configured` and every scheduled job stays off.
- **A FedWiki farm** — required only if you offer FedWiki sites, the
product's first conforming integration; see step 6.
- **Stripe** — required only for paid products; a deployment offering only
free/unmetered products can skip it entirely.
- **Discourse** — optional, for forum-membership sync.
The test stack's `test/compose.yaml` is a useful reference for how these
services relate to each other and to member-console (ports, environment
wiring, health-check ordering) — read it as a **shape example**, not a
supported production compose file. It runs Keycloak, not-yet-hardened
secrets, and `sslmode=disable`, all of which are deliberately wrong for
production.
## 3. Reverse proxy, TLS, and trusted origins
member-console does not terminate TLS itself; put a reverse proxy in front
of it (Caddy, nginx, Traefik, your platform's ingress) that:
- Terminates TLS and forwards plain HTTP to the console.
- Forwards the `Host` header unchanged, or otherwise ensures the console
sees a request whose host matches `base-url`'s host exactly.
- Passes the `Sec-Fetch-Site` and `Origin` headers through unmodified; the
cross-origin check below reads them.
Set `base-url` (`MC_BASE_URL`) to the console's public URL, e.g.
`https://console.example.com`. Three things follow from it at boot:
- Its origin, scheme included, is the console's **sole trusted origin** for
state-changing requests (`net/http.CrossOriginProtection`, wired in
`internal/server/server.go`). A request whose `Sec-Fetch-Site` says
cross-site, or whose `Origin` does not match, is rejected. There is no
separate trusted-origins list, so a reverse proxy that fronts the console at
a second public hostname breaks form posts from that hostname.
- Its scheme decides the session cookie's `Secure` flag: `https` sets it, so
browsers refuse to send the cookie over a plain-HTTP connection. This is
why TLS at the proxy is not optional, and why `base-url` must say `https`
when the proxy serves it: a plain-http `base-url` behind a TLS proxy ships
the cookie without `Secure`.
- The same scheme turns on the CSP `upgrade-insecure-requests` directive and
the `Strict-Transport-Security` header (`max-age=31536000;
includeSubDomains`). HSTS is a commitment: a browser that has seen it will
refuse plain HTTP for the console's hostname, and will treat any
certificate error there as fatal with no click-through, for a year after
its last visit. Keep certificate renewal automatic. `includeSubDomains`
reaches subdomains of the console's host only, which is nothing else at
`console.example.com`; at an apex hostname it would reach every subdomain
you have. If the host must ever go back to plain HTTP, serve
`Strict-Transport-Security: max-age=0` over https first, from the proxy or
by hand, until visitors have picked it up.
`env` (`MC_ENV`) no longer affects any of this; it selects the log format
only (`development` for text, anything else for JSON).
## 4. Identity provider
Follow [identity-provider-setup.md](identity-provider-setup.md) to configure
your OIDC provider: the client, the claim shape, and the `operator-member`
role that gates the operator panel.
**Match the realm's display branding to `deployment-name`.** The sign-in
page a member sees is rendered by the identity provider, not by
member-console — the console only ever redirects to it and receives a token
back. If the console is configured with `deployment-name` (`MC_DEPLOYMENT_NAME`,
see [`internal/config/deployment.go`](../internal/config/deployment.go)) set
to your organization's name, but the IdP's realm still shows Keycloak's
default branding or a different name, a member sees two different
identities across one login flow. Set the realm's display name (Keycloak:
**Realm Settings → General → Display name** / **HTML Display name**, and
its logo/theme if you use one) to the same name as `deployment-name` so the
sign-in page and the app introduce themselves consistently.
## 5. Database
Follow [database-management.md](database-management.md) for the schema
layout and migration conventions. There is nothing extra to run before
first boot: **migrations run automatically at startup** (`member-console
start` calls `db.ConnectAndMigrate` before serving any request —
`cmd/start.go`). A fresh database becomes fully migrated on the first
successful boot; there is no separate "run migrations" step required for a
new deployment, though `member-console migrate up|down|status` is available
for out-of-band migration control (e.g. running the pending migrations before
a rolling restart).
**Check configuration before restarting.** `member-console config validate`
connects to the database, parses every stored override and the resolved
environment against each key's declared type, and exits 1 naming the first
failure and its remediation (or 0 with one line when everything parses); it
never runs migrations and never starts the server. Run it before an
upgrade's restart so a bad value (an environment variable edited by hand, or
an override saved before this check existed) surfaces in your terminal
instead of the service manager's restart loop.
**Manage stored overrides from the terminal.** `member-console config`
offers the operator panel's three override actions from the command line:
`config list [integration]` prints each declared key's environment or
default value, its stored override, and which one wins at the next boot,
secrets masked; `config set <key> <value>` parses a value the same way
the panel does and refuses it with the same sentence on a bad value;
`config clear <key>` deletes a stored override, including one whose key
no installed integration declares. All three reach the same rows the panel
does; a write applies once the app restarts.
**`member-console seed-demo` is not for production.** It inserts a fixed,
idempotent reference catalog (demo products, a plan ladder, sample
grants) so an operator panel has something to click through. Never run it
against a production database.
## 6. Integrations
Configure whichever of these your deployment actually offers. Each is
independently optional; an unconfigured one simply stays dormant (no
routes, no scheduled jobs), it does not block boot.
- **FedWiki** — [fedwiki-setup.md](fedwiki-setup.md) covers the FarmManager
security stack, the farm-side setup, and the `fedwiki-*` keys.
- **Stripe** — see the webhook flow below and [stripe.md](stripe.md) for the
full configuration, subscribed events, and catalog-to-Stripe sync.
- **Discourse** — [discourse-setup.md](discourse-setup.md) covers forum-side
prerequisites and linkage-mode selection.
- **Temporal authorization** — if you run Temporal behind its own JWT
authorizer (recommended), see
[temporal-authorization-setup.md](temporal-authorization-setup.md) for the
IdP-side claim configuration.
Every key any installed integration reads is documented in
[environment-reference.md](environment-reference.md), including which keys
are required together and which default values apply when unset.
### Stripe webhook endpoint and signing secret
Once `stripe-api-key` is set, register a webhook endpoint in the Stripe
Dashboard pointing at `https://<base-url>/webhooks/stripe` and subscribe it
to the event types listed in [stripe.md](stripe.md#webhook-endpoint). Stripe
generates a signing secret (`whsec_...`) for that endpoint when you create
it; set that value as `stripe-webhook-secret` (or point
`stripe-webhook-secret-file` at it). `stripe-api-key` and
`stripe-webhook-secret` are a required pair — the console rejects a boot
where only one is set, and treats both left empty as billing intentionally
disabled.
## 7. First boot
With the database reachable, `base-url` set, the session store's password
configured, and the OIDC keys pointed at your realm, start the process (`member-console
start`, or your image's equivalent entrypoint). At startup it, in order:
runs pending migrations, validates the aggregated configuration (failing
fast with every problem named at once if something required is missing —
see [environment-reference.md](environment-reference.md) for what's
required), connects to Temporal if configured, and starts serving.
Sign in as an operator (the account holding the `operator-member` role you
configured in step 4) and go to `/operator/setup`. It is a checklist of
everything the instance still needs before it has a deliverable product:
entitlement sets, a published product, plan ladders, and — if you configured
Stripe — a resolved billing configuration. Work through it in order; each
step links to the operator page that completes it and reflects as done once
the underlying data exists.