2026-10-29 14:27:26 -04:00
idk
2026-10-26 20:33:28 -04:00
2026-10-29 14:27:26 -04:00
2026-10-23 12:49:53 -04:00
2026-02-19 14:32:48 -05:00
2026-10-23 12:49:53 -04:00
2026-10-29 14:24:47 -04:00
2026-10-29 14:24:47 -04:00
2026-10-29 14:24:47 -04:00
2026-02-19 14:32:48 -05:00
2026-10-23 12:49:53 -04:00
2026-10-23 12:49:53 -04:00
2026-02-19 14:32:48 -05:00
2026-10-29 14:24:47 -04:00
2026-10-23 12:49:53 -04:00

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, 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:

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 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 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 runs real Ansible syntax checks and template rendering without a cluster, then 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.

S
Description
Ansible role to deploy www to Kubernetes
Readme
212 KiB
Languages
Python 53.2%
Jinja 44.9%
Dockerfile 1.9%