225 lines
13 KiB
Markdown
225 lines
13 KiB
Markdown
Role Name
|
||
=========
|
||
|
||
A brief description of the role goes here.
|
||
|
||
Requirements
|
||
# ansible-role-www
|
||
|
||
Deploy the eom.dev website and membership services to **Kubernetes**, not the
|
||
development Docker Compose stack.
|
||
|
||
## What is deployed
|
||
|
||
- Existing namespace, `data` ReadWriteMany archive PVC, Apache, HTTP/Prometheus
|
||
ClusterIP services, TLS ingress and optional Tor OnionService.
|
||
- Website source from `www_repo_url` at `www_repo_version`. Static content is
|
||
copied without deleting the persistent archive.
|
||
- Public membership ConfigMaps, the five existing live Stripe tiers
|
||
($4/$8/$16/$32/$64 USD monthly), optional SHKeeper prepaid crypto and Secret
|
||
references. LDAP/Dex and both payment providers use the same application.
|
||
- Persistent PostgreSQL 17 StatefulSet with a separate ReadWriteOnce claim, or
|
||
an external PostgreSQL server using `verify-full`.
|
||
- A release/configuration-specific migration Job, completed **before** new web
|
||
and worker Deployments are applied. Failed migrations stop the role.
|
||
- Non-root, read-only membership web pods and one reconciliation worker, resource
|
||
limits, probes, optional CA mounts and no service-account token automounts.
|
||
NetworkPolicy allows membership web ingress only from Apache and database
|
||
ingress only from membership clients. Worker ingress is denied.
|
||
|
||
The role does **not** provision LDAP accounts/groups/ACLs, Dex clients, Stripe
|
||
products/webhooks, SHKeeper wallets, image builds or database backups. Review the
|
||
application's deployment instructions before launch.
|
||
|
||
## Requirements
|
||
|
||
- Ansible Core 2.15+ on a Linux/WSL controller, the collections in
|
||
[requirements.yml](requirements.yml), Kubernetes Python client and authorized
|
||
kubeconfig. No application pod needs Kubernetes API permissions.
|
||
- Kubernetes with StatefulSets, Jobs, Ingress/NetworkPolicy, a CNI that enforces
|
||
NetworkPolicy, default persistent StorageClass, and archive RWX provisioner.
|
||
- Ingress-nginx, cert-manager (`ca-issuer` by default), and the Tor controller
|
||
with OnionService CRD when `www_onion_enabled` is true.
|
||
- A **built and pushed** image from the www repository's membership Dockerfile,
|
||
including Stripe and SHKeeper migrations. Set `www_membership_image` to a
|
||
versioned tag, ideally an immutable digest. No unpublished image is assumed
|
||
and nothing is built inside the cluster. The image contract is `/app`,
|
||
Python/manage.py, UID/GID 10001, port 8000 and the Gunicorn default command.
|
||
|
||
## Required configuration
|
||
|
||
Defaults deliberately refuse incomplete membership deployments. Configure:
|
||
|
||
| Variable | Purpose / default |
|
||
| --- | --- |
|
||
| `www_namespace` | Namespace, default `www` |
|
||
| `www_hostname` | Canonical HTTPS hostname, default **www.eom.dev** |
|
||
| `www_membership_enabled` | Deploy membership/proxy, default true |
|
||
| `www_membership_image` | Required versioned application image/digest |
|
||
| `www_membership_secret_name` | Existing Secret, default `membership` |
|
||
| `www_membership_secret_data` | Optional Vault/lookup mapping to manage Secret; default empty |
|
||
| `www_membership_environment_overrides` | Partial public overrides merged with defaults |
|
||
| `www_membership_tiers` | Tier mapping with label, price_id, price_label, usd_amount, group_dn |
|
||
| `www_membership_replicas` | Web replicas, default 2 |
|
||
| `www_membership_worker_interval` | Seconds after full sweep, default 60 |
|
||
| `www_membership_ca_secret_name` | Optional CA Secret mounted at `/etc/membership/ca` |
|
||
| `www_membership_stripe_enabled` | Require Stripe settings/secrets, default true |
|
||
| `www_membership_shkeeper_enabled` | Require SHKeeper settings/key, default false |
|
||
| `www_membership_postgres_managed` | Provision local PostgreSQL, default true |
|
||
| `www_membership_postgres_storage_size` | Default 10Gi |
|
||
| `www_membership_postgres_storage_class` | Empty uses cluster default |
|
||
| `www_repo_url` / `www_repo_version` | Public source URL and ref/commit; default existing URL / HEAD |
|
||
| `www_rollout_revision` | Optional marker to force source/config rollout |
|
||
| `www_image_pull_secrets` | List of `{name: registry-secret}` objects |
|
||
| `www_onion_enabled` | Retain static Tor mirror, default true |
|
||
|
||
Public overrides must include nonempty `LDAP_BIND_DN`, `EMAIL_HOST`, verified
|
||
`LDAP_PASSWORD_SCHEME: '{ARGON2}'`, and the correct Dex issuer/client/connector.
|
||
Stripe needs `STRIPE_PORTAL_CONFIGURATION`. SMTP STARTTLS, production PostgreSQL
|
||
and HTTPS are required; production SQLite is rejected. A complete override of
|
||
`www_membership_environment` is possible but must preserve mandatory defaults.
|
||
|
||
LDAP uses verified LDAPS/StartTLS by default. For deliberate plaintext LDAP
|
||
inside a trusted cluster, set `LDAP_URL: ldap://openldap.openldap.svc.cluster.local:389`
|
||
and `LDAP_ALLOW_INSECURE: "1"` in `www_membership_environment_overrides`, then
|
||
redeploy with a membership image containing this option. This applies to both web
|
||
and worker and sends bind/registration passwords and directory data unencrypted.
|
||
The default is `"0"`; `ldaps://` remains certificate-verified regardless of this
|
||
option. There is no automatic downgrade after a TLS failure.
|
||
|
||
Payment flags validate operator intent; **actual application availability is
|
||
controlled by its environment, secrets and tiers**. Disabling Stripe validation
|
||
alone does not remove its button. Set an empty tier mapping to disable all paid
|
||
tiers. SHKeeper needs both its flag and URL/currencies plus API key in the Secret.
|
||
It prepays one calendar month per invoice, without automatic renewal.
|
||
|
||
Defaults reuse product `prod_Sc9N5J0vNAdLCn` and the same five live Price IDs as
|
||
the application. Level 0 is the free account baseline; paid offers are levels
|
||
1–5. Provision dedicated `www-level-1` through `www-level-5`
|
||
groupOfUniqueNames groups, sentinel and ACLs separately. Override group DNs if
|
||
needed; sandbox testing requires separate test-mode prices.
|
||
|
||
### Secrets
|
||
|
||
Create an Opaque Secret using External Secrets, Sealed Secrets or your secret
|
||
manager, or provide `www_membership_secret_data` from **Ansible Vault**/an external
|
||
lookup. Role-managed values use base64 `data` for repeatable reconciliation.
|
||
Secret create/read/validation tasks use `no_log: true`, and diffs are disabled.
|
||
Base64 is not encryption: enable etcd encryption at rest and appropriate RBAC.
|
||
Never put credentials in public overrides or commit real values to git.
|
||
|
||
Only these keys are permitted:
|
||
|
||
| Secret key | Required |
|
||
| --- | --- |
|
||
| `MEMBERSHIP_SECRET_KEY` | Always; at least 32 random characters |
|
||
| `POSTGRES_PASSWORD` | Always |
|
||
| `DEX_CLIENT_SECRET` | Always |
|
||
| `LDAP_BIND_PASSWORD` | Always |
|
||
| `EMAIL_HOST_PASSWORD` | When SMTP authentication requires it |
|
||
| `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` | With Stripe enabled |
|
||
| `SHKEEPER_API_KEY` | With SHKeeper enabled |
|
||
|
||
Do not use root/admin LDAP credentials. Restrict ACLs to the website account OU
|
||
and dedicated group attributes. Use SHKeeper's invoice API key, never wallet
|
||
private keys or UI/payout credentials.
|
||
|
||
Private trust roots belong in a separate CA Secret. Set paths such as
|
||
`LDAP_CA_CERT_FILE`, `SHKEEPER_CA_CERT_FILE`, `PGSSLROOTCERT` or
|
||
`REQUESTS_CA_BUNDLE` to `/etc/membership/ca/<secret-key>`. The read-only mount is
|
||
shared by web, worker and migrations. Verify certificate expiry, strong keys,
|
||
SHA-2 signatures and trust independently; do not disable TLS verification.
|
||
|
||
### Example inventory and hostname
|
||
|
||
[tests/example-vars.yml](tests/example-vars.yml) contains public configuration
|
||
only. Supply credentials separately via Vault or a pre-existing Secret. Run
|
||
through your existing deployment playbook after pushing matching website and
|
||
membership releases and installing the collection requirements.
|
||
|
||
**Hostname matters:** the old role serves **www.eom.dev**, whereas the standalone
|
||
app defaults to eom.dev. This role sets `MEMBERSHIP_PUBLIC_URL` to
|
||
`https://{{ www_hostname }}`. Register these exact callbacks, including slashes:
|
||
|
||
- Dex: `https://<www_hostname>/members/oidc/callback/`.
|
||
- Stripe: `https://<www_hostname>/members/stripe/webhook/`.
|
||
- SHKeeper: `https://<www_hostname>/members/shkeeper/callback/`.
|
||
|
||
To serve the apex instead, set `www_hostname: eom.dev` and align DNS,
|
||
certificates, callbacks and redirects. Membership requires one canonical HTTPS
|
||
origin for sessions, CSRF, email verification and OIDC.
|
||
|
||
## Upgrades and operations
|
||
|
||
- Pin source ref and images to the same reviewed release. Change
|
||
`www_rollout_revision` when deliberately tracking a moving branch. Git URL/ref
|
||
are passed as environment variables, not shell-interpolated. Never put deploy
|
||
tokens in the repository URL.
|
||
- Apache config/source ref changes replace its pod. Membership image, public
|
||
settings, tiers and Secret/CA **resource versions** trigger a new migration Job
|
||
and workload rollout without publishing secret content hashes. The worker
|
||
uses Recreate to avoid parallel release workers.
|
||
- Completed migration Jobs are retained for idempotent reruns. Prune superseded
|
||
Jobs manually after an upgrade; retain the current Job. Review/remove failed
|
||
Jobs or choose a new rollout revision to retry. Keep schema changes compatible
|
||
with old pods during rollout; coordinate downtime for incompatible changes.
|
||
- The archive PVC is not wiped. Repository files (including dotfiles) overlay
|
||
it; stale deleted static assets are not automatically pruned. Review archive
|
||
path collisions deliberately. Database storage remains separate.
|
||
- PostgreSQL initializes credentials only on an empty directory. Updating the
|
||
Secret **does not rotate an existing DB password**: coordinate SQL rotation,
|
||
application rollout and DB restart manually. Do not change the database
|
||
name/user or major PostgreSQL version on an existing claim without a migration
|
||
plan. Expand existing claims through your approved storage procedure rather
|
||
than editing immutable StatefulSet claim templates.
|
||
- Local PostgreSQL is network-isolated but its default connection is not TLS.
|
||
Use an encrypted private CNI/service mesh or an external database with
|
||
`POSTGRES_SSLMODE=verify-full` and a trusted CA when encrypted DB transport is
|
||
required. Use encrypted disks/backups, restrict access and test restores. The
|
||
single managed instance is not HA and has no automatic backup schedule.
|
||
- For external PostgreSQL set `www_membership_postgres_managed: false` and
|
||
override POSTGRES_HOST/PORT/DB/USER and SSLMODE. Existing managed resources and
|
||
claims are deliberately **not deleted** when switching.
|
||
- Monitor worker errors and stale `last_ldap_sync`, `last_billing_sync` and
|
||
`last_shkeeper_sync`. Process health is not proof of successful reconciliation.
|
||
Downstream apps must refresh cached Dex group/session claims promptly.
|
||
|
||
## Network and privacy
|
||
|
||
No direct membership/database ingress, NodePort or LoadBalancer is created.
|
||
Apache alone may assert the trusted headers. Do not create permissive policies
|
||
or give untrusted pods its label. NetworkPolicy enforcement is required. Egress
|
||
is left to cluster policy: allow DNS, PostgreSQL, LDAP TLS, Dex HTTPS, SMTP
|
||
STARTTLS, Stripe HTTPS and optional SHKeeper HTTPS to configured endpoints.
|
||
Private destinations are supported; the role cannot infer safe IP allowlists.
|
||
|
||
Ingress enforces TLS and disables access logging of email bearer tokens/OIDC
|
||
codes. Apache excludes member access logs; the app image disables Gunicorn
|
||
access logs. Redact URIs/bodies at upstream load balancers, WAFs, APM and error
|
||
tracing too. The default IP throttle sees the ingress peer; configure trusted
|
||
mod_remoteip ranges separately for per-client limits. Never trust arbitrary
|
||
X-Forwarded-For headers.
|
||
|
||
Tor remains a **static mirror**; membership is denied for noncanonical Host
|
||
headers. Disabling membership only disables its Apache route: existing workers,
|
||
database and history are not deleted. Keep workers alive to revoke expiries
|
||
until a reviewed shutdown. Turning off Tor likewise does not delete an existing
|
||
OnionService automatically.
|
||
|
||
## Validation
|
||
|
||
[tests/Dockerfile](tests/Dockerfile) runs real Ansible syntax checks and template
|
||
rendering without a cluster, then
|
||
[tests/validate_manifests.py](tests/validate_manifests.py) asserts Secret/config
|
||
separation, tiers, migrations, private services, network isolation, non-root
|
||
constraints, probes and archive preservation. Fixtures cover managed PostgreSQL,
|
||
external TLS DB/private CA/image pull Secrets, SHKeeper, and disabled membership/
|
||
Tor. Before production, validate in a disposable namespace, including storage
|
||
permissions, NetworkPolicy, migrations, LDAP/Dex and sandbox payment flows.
|
||
|
||
The editor's container scanner currently flags one high-severity finding in the
|
||
validation runner's upstream Python base image, even after updating/pulling it.
|
||
This runner is test-only and is not deployed by the role. Review that finding
|
||
with your authenticated image scanner and refresh approved base-image digests
|
||
before using it in CI; no scanner authentication was configured by this change.
|