317 lines
18 KiB
Markdown
317 lines
18 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.
|
||
|
||
### Service access groups, including free Level 0
|
||
|
||
Set `www_membership_service_groups` to a mapping of **quoted effective level**
|
||
keys to lists of pre-created LDAP group DNs. The default `{}` grants no additional
|
||
groups. For example:
|
||
|
||
```yaml
|
||
www_membership_service_groups:
|
||
"0": ["cn=community-chat,ou=membership-services,dc=example,dc=org"]
|
||
"1": ["cn=community-chat,ou=membership-services,dc=example,dc=org"]
|
||
"2":
|
||
- "cn=community-chat,ou=membership-services,dc=example,dc=org"
|
||
- "cn=game-servers,ou=membership-services,dc=example,dc=org"
|
||
```
|
||
|
||
The highest valid paid membership selects exactly that level's list. **No implicit
|
||
inheritance**: include groups again for levels 3–5 if they should also receive
|
||
them. Level 0 is selected only when no paid entitlement remains; repeat a free
|
||
service DN at every level to offer it to all members. Original tier `group_dn`
|
||
grants are unchanged and independent of these additional service groups.
|
||
|
||
The role serializes the mapping into the existing membership-tiers ConfigMap as
|
||
`service-groups.json`, sets `MEMBERSHIP_SERVICE_GROUPS_FILE`, and includes it in
|
||
the configuration revision so updates trigger workload rollout. Do not override
|
||
it using `MEMBERSHIP_SERVICE_GROUPS_JSON` in the public environment. Publish an
|
||
updated membership image before deploying this role change; no new DB migration
|
||
is needed. See [tests/example-vars.yml](tests/example-vars.yml) for all six levels.
|
||
|
||
Targets must be dedicated `groupOfUniqueNames` groups with the existing sentinel;
|
||
provision groups and narrow `uniqueMember` worker ACLs separately. Configure each
|
||
service/Dex to authorize those groups. Never map admin/staff or independently
|
||
managed groups: the worker **removes noneligible local members**, including
|
||
manually granted memberships. Keep retired groups/ACLs until revocation completes.
|
||
Historical group targets persist for safe removal after mappings change. Expiry
|
||
returns users to level 0 without waiting for Stripe/SHKeeper, but LDAP outages
|
||
and downstream cached sessions can still delay access changes. Free grants cover
|
||
local membership accounts, not untouched users elsewhere in LDAP.
|
||
|
||
### 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.
|
||
|
||
`LDAP_BIND_DN` names the membership service account; `LDAP_BIND_PASSWORD` must
|
||
be its original plaintext password, **not** its stored LDAP `userPassword` hash
|
||
or an already-base64-encoded value. Supply original values in
|
||
`www_membership_secret_data`; the role base64-encodes them once for Secret `data`.
|
||
Current membership login uses verified Dex issuer/subject claims without an LDAP
|
||
connection. These service credentials remain required for account registration
|
||
and worker group synchronization. Older images logging
|
||
`LDAP service bind rejected during Dex callback` or
|
||
`Dex callback rejected kind=LDAPInvalidCredentialsResult` still perform the
|
||
retired callback lookup; upgrade the membership image to remove that dependency.
|
||
Fix service credentials separately for registration/sync, then rerun the role to
|
||
roll out web and worker. Do not use directory administrator credentials.
|
||
`LDAP_PASSWORD_SCHEME` gates new registrations only; it does not select the hash
|
||
algorithm for existing-user login.
|
||
|
||
The Dex-only login release adds migration `0004_dex_login_identity`. Keep
|
||
`DEX_ISSUER` unchanged during this upgrade so existing subjects are pinned to
|
||
their original issuer, and coordinate replacing web/worker together. Existing
|
||
subject bindings and billing data are retained. Unbound local accounts require
|
||
proof from their recent registration session or an administrator's explicit
|
||
`bind_dex_identity` mapping (`--account`, `--uid`, `--subject`); they are never
|
||
claimed by email/username matching alone. Review any retired connector bindings
|
||
before deployment. Keep Dex's existing connector/uid mapping unchanged and do not
|
||
recycle usernames. The worker pins new users' directory UUIDs separately; LDAP
|
||
outages can still delay paid group grants/revocations even though login works.
|
||
|
||
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.
|
||
|
||
### Self-service profiles
|
||
|
||
The profile release requires a membership image with Pillow and migration
|
||
`0005_profile_email_change`, plus the updated website templates/CSS. The role's
|
||
migration Job still runs before workloads. Deploy matching reviewed versions of
|
||
the application and website; do not reuse cached image tags.
|
||
|
||
The membership page links to an authenticated editor with friendly labels for
|
||
the requested inetOrgPerson attributes, with `displayName` labelled **Gamer tag**.
|
||
Dex's normal claims do not contain the full profile; opening the editor reads
|
||
the current LDAP entry, while ordinary sign-in remains LDAP-independent. The
|
||
service identity needs read/search access to profile attributes and operational
|
||
UUID/timestamp fields. Writes use the member's current password and **user bind**,
|
||
not broader service privileges. Configure narrow self-write ACLs, server-side
|
||
mail/uid uniqueness and RFC 4528 assertion support. SSH key edits additionally
|
||
require the `ldapPublicKey` schema and pre-added auxiliary class or an ACL allowing
|
||
only that specific self-add. This role does not create schema/ACLs automatically.
|
||
|
||
Email changes require mailbox verification plus the current password and replace
|
||
all existing mail values. Password hashing is delegated to slapd's Password Modify
|
||
policy; the application does not generate MD5-Crypt or expose stored hashes.
|
||
Review legacy hashing separately. See the application's self-service profile
|
||
documentation for the full attribute mapping and directory prerequisites.
|
||
|
||
The ingress upload limit is 2 MiB to accommodate a 1 MiB JPEG and multipart
|
||
overhead. Apache retains 256 KiB for non-profile member routes. Keep access/body
|
||
logging disabled for verification URLs, passwords and other profile data.
|
||
|
||
### 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.
|