Initial commit

This commit is contained in:
2026-10-23 18:15:49 -04:00
commit 935b3e9f4c
18 changed files with 909 additions and 0 deletions
Executable
+4
View File
@@ -0,0 +1,4 @@
.git
__pycache__
.env*
*.vault
Executable
+6
View File
@@ -0,0 +1,6 @@
root = true
[*.{yml,yaml,j2}]
indent_style = space
indent_size = 2
insert_final_newline = true
Executable
+4
View File
@@ -0,0 +1,4 @@
__pycache__/
*.retry
.env
*.vault
Executable
+194
View File
@@ -0,0 +1,194 @@
# ansible-role-dex
Dedicated Kubernetes/Helm role extracted from ansible-role-eom's Dex task.
Deploys Dex with local **Catppuccin Mocha / Latte** styles and an **eom.dev**
wordmark, retaining Dex's shipped authentication and consent templates.
## Generic defaults and eom.dev workflow
Role defaults now use example domains/DNs, blank credentials, a 10Gi optional
database request, no implicit Matrix client and disabled eom.dev branding.
Plaintext LDAP remains the default as requested; TLS remains opt-in.
Use [playbooks/deploy-eom.yml](playbooks/deploy-eom.yml) for the actual eom.dev
deployment. It calls this role and explicitly preserves the existing issuer,
namespace/releases, database settings/256Gi sizing, plaintext LDAP host, legacy
POSIX/group filter, `uid` subject mapping, Matrix callback, and Catppuccin logo.
Provide these existing variables through controller inventory/group_vars or an
Ansible Vault-encrypted vars file:
- `dex_admin_password`: existing Dex PostgreSQL password.
- `openldap_readonly_password`: existing LDAP search-account password.
- `matrix_dex_oidc_client_id` and `matrix_dex_oidc_client_secret`: existing MAS client.
- `postgresql_admin_password`: needed only if deliberately enabling DB management.
- Optional `www_dex_client_secret`: enables the website client and its separate
`www-ldap`/`entryUUID` connector, without changing legacy identities. It must
match the website's `DEX_CLIENT_SECRET`. Leave absent to preserve Matrix-only
behavior. Review and declare any other deployed clients before rollout.
Run the playbook on the controller with its kubeconfig and this role available
in `ANSIBLE_ROLES_PATH`. Load your encrypted variables with Ansible's `--extra-vars`
file option and `--ask-vault-pass` or your existing Vault identity configuration;
do not put plaintext credentials in this playbook or pass them on command lines.
This is an Ansible playbook, not a CI provider workflow, and is not automatically
triggered. The old EOM compatibility entry now uses generic defaults unless you
explicitly pass these site settings; prefer the site playbook for migration.
## Migration from ansible-role-eom
1. Back up the Dex PostgreSQL database and record the installed Dex/PostgreSQL
Helm release versions and current values in a secure location. Values contain
credentials; do not paste them into tickets, chat or unencrypted git files.
2. Install this role under `ansible-role-dex` in your controller's role search
path. Install [requirements.yml](requirements.yml) and the Kubernetes Python
client; Helm 3 and an authorized kubeconfig are required. Tested with Ansible
Core 2.18; role metadata minimum is 2.15.
3. Use the site workflow above or invoke `ansible-role-dex` with explicit site
variables in the existing playbook. The old
`ansible-role-eom/tasks/dex.yaml` is now a thin `include_role` compatibility
entry point. Use one entry point, not both. No global eom dependency was
added: its current main task only deploys Monero and remains unchanged.
4. Keep namespace `dex`, release `dex`, issuer **https://dex.eom.dev/** (including
trailing slash), database host `postgresql`, user/database `dex` and existing
credentials. A role extraction must not change OIDC issuer or existing client
secrets/redirect URIs, connector IDs or user-ID mappings.
5. Review the pinned Dex chart `0.26.0` (app `v2.46.0`) against your installed
release **before applying**. Override `dex_chart_version` and optionally
`dex_image_tag` if staging a different supported release. This role does not
discover/guess the installed version or perform a live migration by itself.
6. PostgreSQL management defaults to **false**: the existing release and PVC are
left untouched. Do not uninstall the old release or delete its volumes.
The extracted config corrects the old `databbase` typo to `database: dex`.
The site playbook maps legacy inventory variables explicitly:
| Old inventory variable | New role variable |
| --- | --- |
| `dex_admin_password` | `dex_postgres_password` (DB password, not a Dex UI admin) |
| `postgresql_admin_password` | `dex_postgres_admin_password` |
| `openldap_readonly_password` | `dex_ldap_bind_password` |
| `matrix_dex_oidc_client_id` / `matrix_dex_oidc_client_secret` | Existing Matrix Auth Service client |
The site playbook enables the existing Matrix client. Its original
redirect URI is explicit in `dex_matrix_redirect_uris`; override to match your
actual MAS configuration. Other clients previously added directly to Helm must
be declared in `dex_static_clients` before running this role. Helm values are
reset to this role's complete values: undeclared manual Helm customizations are
not implicitly preserved. The role's authoritative values must be reviewed.
## Main variables
See [defaults/main.yml](defaults/main.yml) for all options.
| Variable | Default / behavior |
| --- | --- |
| `dex_namespace`, `dex_release_name` | `dex`, `dex` |
| `dex_chart_version` | `0.26.0` (explicit, reviewed release) |
| `dex_issuer`, `dex_hostname` | `https://{{ dex_hostname }}/`, `dex.example.com` |
| `dex_ingress_class`, `dex_cluster_issuer` | `nginx`, `letsencrypt-prod` |
| `dex_theme_enabled`, `dex_brand_name` | false, `Dex` (site playbook enables eom.dev theme) |
| `dex_static_clients` | Additional Dex static client objects; default empty |
| `dex_additional_connectors` | Additional complete connector objects; default empty |
| `dex_env_secret_name` | Optional existing environment Secret in Dex namespace |
| `dex_ca_secret_name` | Optional existing CA Secret mounted at `/etc/dex-ca` |
| `dex_postgres_manage` | false; reuse existing DB without changing its release |
| `dex_postgres_ssl_mode` | `disable`, preserving internal DB behavior |
| `dex_image_pull_secrets` | Kubernetes imagePullSecrets list |
| `dex_extra_volumes`, `dex_extra_volume_mounts` | Optional additional mounts |
### LDAP: plaintext is the default, TLS is opt-in
As requested, default LDAP behavior remains `insecureNoSSL: true` against
`ldap.example.com:389` (the site playbook uses
`openldap.openldap.svc.cluster.local`). This exposes both service-bind and user
passwords to network observers. Restrict it to the trusted internal network with
your cluster's policies; no public LDAP exposure is created by this role.
The generic connector uses `inetOrgPerson` and DN-based group matching. The site
playbook retains ID `ldap`, `idAttr: uid`, its POSIX/group filter,
and `userAttr: cn` group matching. For `uniqueMember` values containing full DNs,
set `dex_ldap_group_user_attribute: DN` deliberately after testing. Changing
connector ID or idAttr changes OIDC subjects and can detach existing accounts.
For **StartTLS**, explicitly set `dex_ldap_insecure_no_ssl: false`,
`dex_ldap_start_tls: true`, and a host/port supporting StartTLS. For **LDAPS**,
set both flags false and use the server's TLS hostname with port 636. Certificate
verification remains enabled; no `insecureSkipVerify` bypass is offered. For a
private CA set `dex_ca_secret_name` and `dex_ldap_root_ca` to
`/etc/dex-ca/<secret-key>`. Verify certificate hostname, validity, SHA-2 signature,
strong keys and CA trust independently. Additional connectors have their own
transport settings and must be configured separately.
### Credentials and clients
Supply database and LDAP passwords from Vault/external lookups or the supported
legacy secret variables. Helm deployment and validation tasks are `no_log` and
diff-suppressed. Dex configuration is stored by the official chart in a Kubernetes
**Secret**, never in the theme ConfigMap. Helm stores release values, including
inline credentials, in its release Secrets: protect these with RBAC, encryption
at rest and encrypted backups. This role does not generate or rotate passwords.
Client `secretEnv` can refer to a key in `dex_env_secret_name`. LDAP connectors
support `$ENV_NAME` references, such as `bindPW: $DEX_LDAP_BIND_PASSWORD`. The
database password remains a protected Helm/Dex config value; it is not assumed
to support the same connector substitution. Never commit actual secrets.
[tests/example-vars.yml](tests/example-vars.yml) shows a www client and separate
`www-ldap` connector using `entryUUID`, `inetOrgPerson` and DN group matching.
This avoids breaking legacy subjects while supporting new free website users.
The Secret's `WWW_DEX_CLIENT_SECRET` must match the website's `DEX_CLIENT_SECRET`;
set the website's `DEX_CONNECTOR_ID=www-ldap` and exact matching issuer. No new
client or connector is silently enabled by default. Verify that this connector's
search base includes the website account OU before enabling public registration.
### Theme
[files/styles.css](files/styles.css) implements responsive Mocha dark mode and
Latte light mode using `prefers-color-scheme`, visible focus rings, local fonts,
inputs, errors, connector buttons and consent panels. [files/logo.svg](files/logo.svg)
extends the website's orbit mark into an eom.dev wordmark.
Only `themes/light/styles.css` and `themes/light/logo.svg` are mounted read-only
under the official image's `/srv/dex/web`; frontend selects that directory and
`theme/logo.svg`. All other version-matched templates, icons and static assets
come from the installed Dex image. No forms, OAuth parameters, redirects or
approval/security logic are replaced. This deliberately retains the shipped
favicon; logo/theme are local, with no third-party fonts or scripts.
A checksum of public theme assets triggers a rollout when they change (subPath
mounts do not hot-reload). Setting `dex_theme_enabled: false` restores Dex's
default frontend; the unused ConfigMap is retained. Existing environment Secret
or CA Secret rotations require a Dex rollout; the role does not expose or hash
their contents. Inspect log privacy at any upstream proxies too: ingress access
logs are disabled to avoid recording authorization codes and related URLs.
### Optional PostgreSQL management
For a new install or an explicitly reviewed DB upgrade, set
`dex_postgres_manage: true` and **pin `dex_postgres_chart_version`** to the intended
Bitnami release. Defaults retain release `postgresql`, 256Gi and `volumeName: data`
from the old task. Confirm existing PV/PVC binding, storage size, chart/image
availability and schema compatibility first; do not use this to switch major
database versions without a backup/restore plan. Changing Helm password values
does not automatically rotate an existing database password. Managed DB transport
is unchanged; configure external DB TLS/mesh separately if required.
## Validation
[tests/Dockerfile](tests/Dockerfile) installs the declared collection and Helm,
checks Ansible syntax, renders actual values and runs the pinned chart through
Helm and strict Kubernetes schemas. Fixtures verify legacy plaintext/subjects,
Matrix client, additional environment client, opt-in TLS, theme mounts, protected
Dex config and disabled/minimal variants. No cluster is contacted by these tests.
The theme was also exercised with the official Dex v2.46.0 image using isolated
SQLite/mock authentication. Verify real LDAP, client redirects and database
access in staging before rollout. Chart upgrades should repeat that image/theme
compatibility test. No production deployments or database changes are performed
as part of this extraction.
The editor's scanner flags high-severity findings in the validation runner's
upstream Python base image. That image is test-only and is not deployed by this
role. Review/update approved CI base-image digests with your image scanner;
these findings are not represented as resolved by the functional tests.
+65
View File
@@ -0,0 +1,65 @@
---
dex_namespace: dex
dex_release_name: dex
dex_chart_version: "0.26.0"
dex_issuer: "https://{{ dex_hostname }}/"
dex_hostname: dex.example.com
dex_ingress_class: nginx
dex_cluster_issuer: letsencrypt-prod
dex_tls_secret_name: dex
dex_wait_timeout: 600s
dex_image_tag: "" # Empty uses the pinned chart's application version.
dex_image_pull_secrets: []
dex_resources:
requests: {cpu: 100m, memory: 128Mi}
limits: {cpu: "1", memory: 512Mi}
# Reuse the existing postgresql release/PVC. Never upgrade it implicitly.
dex_postgres_manage: false
dex_postgres_release_name: postgresql
dex_postgres_chart_version: "" # Required if explicitly managing Bitnami PostgreSQL.
dex_postgres_storage_size: 10Gi
dex_postgres_host: postgresql
dex_postgres_port: 5432
dex_postgres_database: dex
dex_postgres_user: dex
dex_postgres_password: ""
dex_postgres_admin_password: ""
dex_postgres_ssl_mode: disable
# Preserve legacy internal plaintext LDAP behavior. TLS is explicitly opt-in.
# Plaintext exposes bind/user passwords; restrict this traffic to trusted networks.
dex_ldap_host: ldap.example.com:389
dex_ldap_insecure_no_ssl: true
dex_ldap_start_tls: false
dex_ldap_bind_dn: cn=readonly,dc=example,dc=com
dex_ldap_bind_password: ""
dex_ldap_root_ca: ""
dex_ca_secret_name: ""
dex_ldap_connector_id: ldap
dex_ldap_connector_name: Account
dex_ldap_user_base_dn: ou=people,dc=example,dc=com
dex_ldap_user_filter: "(objectClass=inetOrgPerson)"
dex_ldap_id_attribute: uid
dex_ldap_name_attribute: cn
dex_ldap_group_base_dn: ou=groups,dc=example,dc=com
# groupOfUniqueNames stores full user DNs in uniqueMember.
dex_ldap_group_user_attribute: DN
dex_ldap_group_filter: "(objectClass=groupOfUniqueNames)"
# Optional legacy Matrix convenience client. Configure its ID/secret explicitly.
dex_matrix_client_enabled: false
dex_matrix_redirect_uris: []
# Full objects (id, name, secret or secretEnv, redirectURIs). Use Vault for secrets.
dex_static_clients: []
# Additional connectors let www use entryUUID without changing legacy subjects.
dex_additional_connectors: []
# Optional environment Secret for secretEnv and $ENV connector credentials.
dex_env_secret_name: ""
dex_extra_volumes: []
dex_extra_volume_mounts: []
# eom.dev artwork is opt-in via the deployment playbook.
dex_theme_enabled: false
dex_brand_name: Dex
dex_theme_configmap_name: dex-catppuccin
Executable
+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" width="320" height="96" viewBox="0 0 320 96" role="img" aria-labelledby="title">
<title id="title">eom.dev — One account. Your community.</title>
<style>
.word { fill: #cdd6f4; }
.tagline { fill: #a6adc8; }
.dot { fill: #cba6f7; }
@media (prefers-color-scheme: light) {
.word { fill: #4c4f69; }
.tagline { fill: #5c5f77; }
.dot { fill: #8839ef; }
}
</style>
<g transform="translate(6 16)">
<circle cx="32" cy="32" r="30" fill="#181825" stroke="#585b70" stroke-width="1.5"/>
<g fill="none" stroke-width="2.5">
<ellipse cx="32" cy="32" rx="27" ry="13" stroke="#cba6f7" transform="rotate(-28 32 32)"/>
<ellipse cx="32" cy="32" rx="27" ry="13" stroke="#89b4fa" transform="rotate(32 32 32)"/>
<ellipse cx="32" cy="32" rx="20" ry="10" stroke="#a6e3a1" transform="rotate(72 32 32)"/>
</g>
<circle cx="32" cy="32" r="11" fill="#1e1e2e" stroke="#585b70"/>
</g>
<text x="88" y="51" font-family="system-ui, sans-serif" font-size="39" font-weight="800" letter-spacing="-1.5"><tspan class="word">eom</tspan><tspan class="dot">.</tspan><tspan class="word">dev</tspan></text>
<text class="tagline" x="90" y="75" font-family="system-ui, sans-serif" font-size="12">One account. Your community.</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+105
View File
@@ -0,0 +1,105 @@
/* Catppuccin Mocha / Latte. Local assets only; Dex forms/templates unchanged. */
:root {
color-scheme: dark;
--base: #1e1e2e;
--mantle: #181825;
--surface: #313244;
--border: #585b70;
--text: #cdd6f4;
--muted: #a6adc8;
--accent: #cba6f7;
--accent-hover: #b4befe;
--error: #f38ba8;
--focus: #fab387;
}
@media (prefers-color-scheme: light) {
:root {
color-scheme: light;
--base: #eff1f5;
--mantle: #e6e9ef;
--surface: #fff;
--border: #9ca0b0;
--text: #4c4f69;
--muted: #5c5f77;
--accent: #8839ef;
--accent-hover: #7287fd;
--error: #d20f39;
--focus: #fe640b;
}
}
* { box-sizing: border-box; }
.theme-body {
margin: 0;
min-height: 100vh;
background: var(--base);
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.6;
}
.theme-navbar {
background: var(--mantle);
border-bottom: 1px solid var(--border);
padding: 1.4rem;
text-align: center;
}
.theme-navbar__logo-wrap { display: inline-block; max-width: 100%; }
.theme-navbar__logo { display: block; width: 240px; height: 72px; max-width: 100%; }
.dex-container {
color: var(--text);
width: min(500px, calc(100% - 2rem));
min-width: 0;
margin: clamp(2rem, 8vh, 5rem) auto;
}
.theme-panel {
padding: clamp(1.4rem, 5vw, 2.6rem);
background: var(--mantle);
border: 1px solid var(--border);
border-radius: 1.25rem;
box-shadow: 0 12px 40px rgb(0 0 0 / 10%);
}
.theme-heading { font-size: 1.6rem; font-weight: 750; line-height: 1.25; margin: 0 0 1.2rem; }
.theme-form-row { display: block; margin: 1.1rem auto; }
.theme-form-label { display: block; text-align: left; width: 100%; max-width: 320px; margin: 0 auto .4rem; font-size: .95rem; font-weight: 600; }
.theme-form-input {
display: block;
width: 100%;
max-width: 320px;
min-height: 48px;
margin: auto;
padding: .75rem 1rem;
background: var(--base);
color: var(--text);
border: 1px solid var(--border);
border-radius: .65rem;
font: inherit;
}
.theme-form-input::placeholder { color: var(--muted); }
.dex-btn { border-radius: .65rem; box-shadow: none; font: inherit; max-width: 100%; }
.theme-btn--primary, .theme-btn--success, .theme-btn-provider {
background: var(--accent);
color: var(--mantle);
border: 1px solid transparent;
min-height: 48px;
min-width: 0;
width: 100%;
max-width: 320px;
padding: .5rem 1rem;
font-weight: 700;
}
.theme-btn-provider { background: var(--surface); color: var(--text); border-color: var(--border); }
.theme-btn--primary:hover, .theme-btn--success:hover { background: var(--accent-hover); }
.theme-btn-provider:hover { border-color: var(--accent); }
.dex-btn-icon { border-radius: .3rem; margin: 0 .5rem 0 0; }
.dex-btn-text { line-height: 2rem; padding: .2rem; }
a { color: var(--accent); text-underline-offset: .2em; }
a:hover { color: var(--accent-hover); }
.theme-link-back { margin-top: 1rem; }
.dex-subtle-text, .dex-separator, .dex-list { color: var(--muted); }
.dex-error-box { background: var(--surface); color: var(--error); border: 1px solid var(--error); border-radius: .6rem; padding: .7rem; max-width: 100%; }
.theme-form-input:focus-visible, .dex-btn:focus-visible, a:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 3px;
}
@media (prefers-color-scheme: light) {
.theme-btn--primary, .theme-btn--success { color: #fff; }
}
Executable
+7
View File
@@ -0,0 +1,7 @@
---
galaxy_info:
author: Eric Meehan
description: Dex Helm deployment with Catppuccin and eom.dev branding
license: MIT-0
min_ansible_version: "2.15"
dependencies: []
+82
View File
@@ -0,0 +1,82 @@
---
# Run on the controller with its existing kubeconfig; no SSH target is required.
# Supply credentials via inventory/group_vars or a Vault-encrypted vars file.
- name: Deploy eom.dev Dex with Catppuccin branding
hosts: localhost
connection: local
gather_facts: false
vars:
dex_namespace: dex
dex_release_name: dex
dex_chart_version: "0.26.0"
dex_issuer: https://dex.eom.dev/
dex_hostname: dex.eom.dev
dex_ingress_class: nginx
dex_cluster_issuer: ca-issuer
dex_tls_secret_name: dex
dex_theme_enabled: true
dex_brand_name: eom.dev
dex_theme_configmap_name: dex-catppuccin
# Preserve the existing DB release and data; do not upgrade it by default.
dex_postgres_manage: false
dex_postgres_release_name: postgresql
dex_postgres_host: postgresql
dex_postgres_port: 5432
dex_postgres_database: dex
dex_postgres_user: dex
dex_postgres_storage_size: 256Gi
dex_postgres_ssl_mode: disable
dex_postgres_password: "{{ dex_admin_password }}"
dex_postgres_admin_password: "{{ postgresql_admin_password | default('') }}"
# Explicitly preserve internal plaintext LDAP and legacy OIDC subjects.
dex_ldap_host: openldap.openldap.svc.cluster.local
dex_ldap_insecure_no_ssl: true
dex_ldap_start_tls: false
dex_ldap_bind_dn: cn=readonly,dc=eom,dc=dev
dex_ldap_bind_password: "{{ openldap_readonly_password }}"
dex_ldap_connector_id: ldap
dex_ldap_connector_name: eom.dev account
dex_ldap_user_base_dn: dc=eom,dc=dev
dex_ldap_user_filter: "(&(objectClass=posixAccount)(memberOf=cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev))"
dex_ldap_id_attribute: uid
dex_ldap_name_attribute: givenName
dex_ldap_group_base_dn: dc=eom,dc=dev
dex_ldap_group_user_attribute: cn
dex_ldap_group_filter: "(objectClass=groupOfUniqueNames)"
dex_matrix_client_enabled: true
dex_matrix_redirect_uris:
- https://mas.eom.dev/upstream/callback/01K96AQEZKKABW34PY3R6BVNJ4
# Optional website integration. Set www_dex_client_secret in Vault to enable;
# it must equal the membership application's DEX_CLIENT_SECRET.
dex_static_clients: "{{ [{'id': 'www-membership', 'name': 'eom.dev Membership', 'secret': www_dex_client_secret, 'redirectURIs': ['https://www.eom.dev/members/oidc/callback/']}] if (www_dex_client_secret | default('') | length > 0) else [] }}"
dex_additional_connectors: "{{ [_dex_eom_www_connector] if (www_dex_client_secret | default('') | length > 0) else [] }}"
_dex_eom_www_connector:
type: ldap
id: www-ldap
name: eom.dev Membership
config:
host: openldap.openldap.svc.cluster.local
insecureNoSSL: true
bindDN: cn=readonly,dc=eom,dc=dev
bindPW: "{{ openldap_readonly_password }}"
userSearch:
baseDN: ou=people,dc=eom,dc=dev
filter: "(objectClass=inetOrgPerson)"
username: uid
idAttr: entryUUID
emailAttr: mail
nameAttr: cn
preferredUsernameAttr: uid
groupSearch:
baseDN: ou=groups,dc=eom,dc=dev
filter: "(objectClass=groupOfUniqueNames)"
userMatchers:
- userAttr: DN
groupAttr: uniqueMember
nameAttr: cn
roles:
- ansible-role-dex
+2
View File
@@ -0,0 +1,2 @@
---
collections: [{name: kubernetes.core, version: ">=3.0.0,<7.0.0"}]
Executable
+61
View File
@@ -0,0 +1,61 @@
---
- name: Validate Dex deployment configuration
ansible.builtin.assert:
that:
- dex_issuer is match('^https://[^/@]+')
- dex_hostname is match('^[a-z0-9][a-z0-9.-]*[a-z0-9]$')
- dex_postgres_password | length > 0
- dex_ldap_bind_password | length > 0
- not (dex_ldap_insecure_no_ssl | bool and dex_ldap_start_tls | bool)
- not dex_postgres_manage or dex_postgres_chart_version | length > 0
- not dex_postgres_manage or dex_postgres_admin_password | length > 0
- not dex_matrix_client_enabled or matrix_dex_oidc_client_secret | default('') | length > 0
fail_msg: Configure database/LDAP credentials, client secrets and explicit database chart version if managing PostgreSQL.
no_log: true
- name: Ensure Dex namespace
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ dex_namespace }}"
- name: Register Dex Helm repository
kubernetes.core.helm_repository:
name: dex
repo_url: https://charts.dexidp.io
- name: Manage PostgreSQL only when explicitly requested
ansible.builtin.include_tasks: postgres.yml
when: dex_postgres_manage | bool
- name: Deploy Catppuccin theme assets
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: ConfigMap
metadata:
name: "{{ dex_theme_configmap_name }}"
namespace: "{{ dex_namespace }}"
data:
styles.css: "{{ lookup('ansible.builtin.file', role_path ~ '/files/styles.css') }}"
logo.svg: "{{ lookup('ansible.builtin.file', role_path ~ '/files/logo.svg') }}"
when: dex_theme_enabled | bool
- name: Deploy Dex using its existing Helm release
kubernetes.core.helm:
name: "{{ dex_release_name }}"
chart_ref: dex/dex
chart_version: "{{ dex_chart_version }}"
release_namespace: "{{ dex_namespace }}"
create_namespace: true
update_repo_cache: true
reset_values: true
wait: true
timeout: "{{ dex_wait_timeout }}"
values: "{{ lookup('ansible.builtin.template', 'values.yml.j2') | from_yaml }}"
no_log: true
diff: false
+32
View File
@@ -0,0 +1,32 @@
---
- name: Register Bitnami repository for optional database management
kubernetes.core.helm_repository:
name: bitnami
repo_url: https://charts.bitnami.com/bitnami
- name: Deploy explicitly versioned existing PostgreSQL release
kubernetes.core.helm:
name: "{{ dex_postgres_release_name }}"
chart_ref: bitnami/postgresql
chart_version: "{{ dex_postgres_chart_version }}"
release_namespace: "{{ dex_namespace }}"
create_namespace: true
wait: true
timeout: "{{ dex_wait_timeout }}"
values:
auth:
enablePostgresUser: true
postgresPassword: "{{ dex_postgres_admin_password }}"
username: "{{ dex_postgres_user }}"
password: "{{ dex_postgres_password }}"
database: "{{ dex_postgres_database }}"
primary:
name: primary
initdb:
scripts: {}
persistence:
enabled: true
volumeName: data
size: "{{ dex_postgres_storage_size }}"
no_log: true
diff: false
+135
View File
@@ -0,0 +1,135 @@
replicaCount: 1
image:
tag: {{ dex_image_tag | to_json }}
imagePullSecrets: {{ dex_image_pull_secrets | to_json }}
rbac:
create: false
resources: {{ dex_resources | to_json }}
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
podAnnotations:
theme/revision: {{ (lookup('ansible.builtin.file', role_path ~ '/files/styles.css') ~ lookup('ansible.builtin.file', role_path ~ '/files/logo.svg') ~ (dex_theme_enabled | string) ~ dex_env_secret_name ~ dex_ca_secret_name) | hash('sha256') | to_json }}
{% if dex_env_secret_name %}
envFrom:
- secretRef:
name: {{ dex_env_secret_name | to_json }}
{% endif %}
config:
issuer: {{ dex_issuer | to_json }}
logger:
level: info
format: json
{% if dex_theme_enabled %}
frontend:
issuer: {{ dex_brand_name | to_json }}
dir: /srv/dex/web
theme: light
logoURL: theme/logo.svg
{% endif %}
storage:
type: postgres
config:
host: {{ dex_postgres_host | to_json }}
port: {{ dex_postgres_port }}
database: {{ dex_postgres_database | to_json }}
user: {{ dex_postgres_user | to_json }}
password: {{ dex_postgres_password | to_json }}
ssl:
mode: {{ dex_postgres_ssl_mode | to_json }}
staticClients:{% if not dex_matrix_client_enabled and not dex_static_clients %} []{% endif %}
{% if dex_matrix_client_enabled %}
- id: {{ matrix_dex_oidc_client_id | to_json }}
name: Matrix Auth Service
secret: {{ matrix_dex_oidc_client_secret | to_json }}
redirectURIs: {{ dex_matrix_redirect_uris | to_json }}
{% endif %}
{% for client in dex_static_clients %}
- {{ client | to_json }}
{% endfor %}
connectors:
- type: ldap
id: {{ dex_ldap_connector_id | to_json }}
name: {{ dex_ldap_connector_name | to_json }}
config:
host: {{ dex_ldap_host | to_json }}
insecureNoSSL: {{ dex_ldap_insecure_no_ssl | bool | to_json }}
startTLS: {{ dex_ldap_start_tls | bool | to_json }}
insecureSkipVerify: false
{% if dex_ldap_root_ca %}
rootCA: {{ dex_ldap_root_ca | to_json }}
{% endif %}
bindDN: {{ dex_ldap_bind_dn | to_json }}
bindPW: {{ dex_ldap_bind_password | to_json }}
userSearch:
baseDN: {{ dex_ldap_user_base_dn | to_json }}
filter: {{ dex_ldap_user_filter | to_json }}
username: uid
idAttr: {{ dex_ldap_id_attribute | to_json }}
emailAttr: mail
nameAttr: {{ dex_ldap_name_attribute | to_json }}
preferredUsernameAttr: uid
groupSearch:
baseDN: {{ dex_ldap_group_base_dn | to_json }}
filter: {{ dex_ldap_group_filter | to_json }}
userMatchers:
- userAttr: {{ dex_ldap_group_user_attribute | to_json }}
groupAttr: uniqueMember
nameAttr: cn
{% for connector in dex_additional_connectors %}
- {{ connector | to_json }}
{% endfor %}
volumes:{% if not dex_theme_enabled and not dex_ca_secret_name and not dex_extra_volumes %} []{% endif %}
{% if dex_theme_enabled %}
- name: theme
configMap:
name: {{ dex_theme_configmap_name }}
{% endif %}
{% if dex_ca_secret_name %}
- name: ca
secret:
secretName: {{ dex_ca_secret_name }}
{% endif %}
{% for volume in dex_extra_volumes %}
- {{ volume | to_json }}
{% endfor %}
volumeMounts:{% if not dex_theme_enabled and not dex_ca_secret_name and not dex_extra_volume_mounts %} []{% endif %}
{% if dex_theme_enabled %}
- name: theme
mountPath: /srv/dex/web/themes/light/styles.css
subPath: styles.css
readOnly: true
- name: theme
mountPath: /srv/dex/web/themes/light/logo.svg
subPath: logo.svg
readOnly: true
{% endif %}
{% if dex_ca_secret_name %}
- name: ca
mountPath: /etc/dex-ca
readOnly: true
{% endif %}
{% for mount in dex_extra_volume_mounts %}
- {{ mount | to_json }}
{% endfor %}
ingress:
enabled: true
className: {{ dex_ingress_class | to_json }}
annotations:
cert-manager.io/cluster-issuer: {{ dex_cluster_issuer | to_json }}
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/enable-access-log: "false"
hosts:
- host: {{ dex_hostname }}
paths:
- path: /
pathType: Prefix
tls:
- hosts: [{{ dex_hostname }}]
secretName: {{ dex_tls_secret_name }}
+10
View File
@@ -0,0 +1,10 @@
FROM python:3.13-slim-trixie
RUN pip install --no-cache-dir 'ansible-core>=2.18,<2.19' 'kubernetes>=28,<34' PyYAML 'kubernetes-validate>=1.31,<1.35'
ADD https://get.helm.sh/helm-v3.17.3-linux-amd64.tar.gz /tmp/helm.tar.gz
RUN echo 'ee88b3c851ae6466a3de507f7be73fe94d54cbf2987cbaa3d1a3832ea331f2cd /tmp/helm.tar.gz' | sha256sum -c - && tar -xzf /tmp/helm.tar.gz -C /tmp && mv /tmp/linux-amd64/helm /usr/local/bin/helm && rm -rf /tmp/linux-amd64 /tmp/helm.tar.gz
COPY requirements.yml /tmp/requirements.yml
RUN ansible-galaxy collection install -r /tmp/requirements.yml
WORKDIR /roles/ansible-role-dex
COPY . ./
ENV ANSIBLE_ROLES_PATH=/roles
CMD ["sh", "-ec", "ansible-playbook -i localhost, tests/test.yml --syntax-check; ansible-playbook -i localhost, playbooks/deploy-eom.yml --syntax-check; ansible-playbook -i localhost, tests/render.yml; helm repo add dex https://charts.dexidp.io; helm template dex dex/dex --version 0.26.0 --namespace dex -f /tmp/dex-values.yml > /tmp/dex-manifests.yml; python tests/validate.py"]
+48
View File
@@ -0,0 +1,48 @@
---
# Passwords supplied in a Vault-encrypted inventory, never in this example.
dex_issuer: https://dex.eom.dev/
dex_hostname: dex.eom.dev
dex_postgres_manage: false
dex_postgres_host: postgresql
dex_postgres_database: dex
dex_postgres_user: dex
# Legacy credential names are still supported by defaults:
# dex_admin_password, openldap_readonly_password, matrix_dex_oidc_client_secret.
# Existing Secret in the Dex namespace. WWW_DEX_CLIENT_SECRET must match the
# www membership app's DEX_CLIENT_SECRET. DEX_LDAP_BIND_PASSWORD is the search
# account password; no real values go in this public file.
dex_env_secret_name: dex-client-secrets
dex_static_clients:
- id: www-membership
name: eom.dev Membership
secretEnv: WWW_DEX_CLIENT_SECRET
redirectURIs:
- https://www.eom.dev/members/oidc/callback/
# Additional connector: don't alter legacy ldap/idAttr uid, which changes sub
# identifiers for existing applications. This connector admits free web accounts.
dex_additional_connectors:
- type: ldap
id: www-ldap
name: eom.dev Membership
config:
host: openldap.openldap.svc.cluster.local
insecureNoSSL: true
bindDN: cn=readonly,dc=eom,dc=dev
bindPW: $DEX_LDAP_BIND_PASSWORD
userSearch:
baseDN: ou=people,dc=eom,dc=dev
filter: "(objectClass=inetOrgPerson)"
username: uid
idAttr: entryUUID
emailAttr: mail
nameAttr: cn
preferredUsernameAttr: uid
groupSearch:
baseDN: ou=groups,dc=eom,dc=dev
filter: "(objectClass=groupOfUniqueNames)"
userMatchers:
- userAttr: DN
groupAttr: uniqueMember
nameAttr: cn
+52
View File
@@ -0,0 +1,52 @@
---
- name: Render Dex values without a cluster or real secrets
hosts: localhost
connection: local
gather_facts: false
vars:
role_path: /roles/ansible-role-dex
tasks:
- name: Load defaults
ansible.builtin.include_vars: ../defaults/main.yml
- name: Set fake isolated credentials
ansible.builtin.set_fact:
dex_postgres_password: fixture-not-a-real-password
dex_ldap_bind_password: fixture-not-a-real-password
dex_matrix_client_enabled: true
matrix_dex_oidc_client_id: matrix-fixture
matrix_dex_oidc_client_secret: fixture-not-a-real-client-secret
dex_theme_enabled: true
- name: Render default values
ansible.builtin.template:
src: ../templates/values.yml.j2
dest: /tmp/dex-values.yml
mode: "0600"
- name: Set optional settings
ansible.builtin.set_fact:
dex_theme_enabled: false
dex_matrix_client_enabled: false
dex_ldap_insecure_no_ssl: false
dex_ldap_start_tls: true
dex_ldap_root_ca: /etc/dex-ca/ldap.pem
dex_ca_secret_name: ldap-ca
dex_env_secret_name: dex-client-secrets
dex_static_clients:
- id: www-membership
name: Example Membership
secretEnv: WWW_DEX_CLIENT_SECRET
redirectURIs: [https://www.example.com/members/oidc/callback/]
- name: Render optional values
ansible.builtin.template:
src: ../templates/values.yml.j2
dest: /tmp/dex-values-optional.yml
mode: "0600"
- name: Set empty optional lists
ansible.builtin.set_fact:
dex_ca_secret_name: ""
dex_env_secret_name: ""
dex_static_clients: []
- name: Render minimal values
ansible.builtin.template:
src: ../templates/values.yml.j2
dest: /tmp/dex-values-minimal.yml
mode: "0600"
Executable
+6
View File
@@ -0,0 +1,6 @@
---
- name: Check dedicated Dex role syntax
hosts: localhost
gather_facts: false
roles:
- ansible-role-dex
+72
View File
@@ -0,0 +1,72 @@
"""Check real Ansible-rendered values and Helm manifests without a cluster."""
import base64
from pathlib import Path
import yaml
from kubernetes_validate import validate
from ansible.parsing.dataloader import DataLoader
from ansible.template import Templar
# Resolve the site's actual workflow variables with fixture credentials only;
# verify both Matrix-only migration and opt-in website integration.
defaults = yaml.safe_load(Path('defaults/main.yml').read_text())
site = yaml.safe_load(Path('playbooks/deploy-eom.yml').read_text())[0]['vars']
for website_enabled in (False, True):
variables = {
**defaults, **site,
'dex_admin_password': 'fixture-db',
'openldap_readonly_password': 'fixture-ldap',
'matrix_dex_oidc_client_id': 'matrix-fixture',
'matrix_dex_oidc_client_secret': 'fixture-matrix',
}
if website_enabled:
variables['www_dex_client_secret'] = 'fixture-www'
resolved = Templar(loader=DataLoader(), variables=variables).template(site)
assert resolved['dex_issuer'] == 'https://dex.eom.dev/'
assert resolved['dex_postgres_password'] == 'fixture-db'
assert resolved['dex_ldap_group_user_attribute'] == 'cn'
assert resolved['dex_theme_enabled'] is True
assert resolved['dex_postgres_manage'] is False
assert bool(resolved['dex_static_clients']) == website_enabled
assert bool(resolved['dex_additional_connectors']) == website_enabled
if website_enabled:
assert resolved['dex_static_clients'][0]['secret'] == 'fixture-www'
assert resolved['dex_additional_connectors'][0]['config']['userSearch']['idAttr'] == 'entryUUID'
values = yaml.safe_load(Path('/tmp/dex-values.yml').read_text())
config = values['config']
assert config['issuer'] == 'https://dex.example.com/'
assert config['storage']['config']['database'] == 'dex'
assert 'databbase' not in config['storage']['config']
ldap = config['connectors'][0]
assert ldap['id'] == 'ldap'
assert ldap['config']['insecureNoSSL'] is True
assert ldap['config']['startTLS'] is False
assert ldap['config']['userSearch']['idAttr'] == 'uid'
assert ldap['config']['groupSearch']['userMatchers'][0]['userAttr'] == 'DN'
assert config['staticClients'][0]['id'] == 'matrix-fixture'
assert config['frontend']['logoURL'] == 'theme/logo.svg'
assert config['frontend']['dir'] == '/srv/dex/web'
assert len(values['volumeMounts']) == 2
assert all(mount['readOnly'] for mount in values['volumeMounts'])
optional = yaml.safe_load(Path('/tmp/dex-values-optional.yml').read_text())
assert 'frontend' not in optional['config']
assert optional['config']['connectors'][0]['config']['startTLS'] is True
assert optional['config']['staticClients'][0]['secretEnv'] == 'WWW_DEX_CLIENT_SECRET'
assert optional['envFrom'][0]['secretRef']['name'] == 'dex-client-secrets'
minimal = yaml.safe_load(Path('/tmp/dex-values-minimal.yml').read_text())
assert minimal['config']['staticClients'] == []
assert minimal['volumes'] == [] and minimal['volumeMounts'] == []
objects = [obj for obj in yaml.safe_load_all(Path('/tmp/dex-manifests.yml').read_text()) if obj]
for obj in objects:
validate(obj, desired_version='1.31.0', strict=True)
deployment = next(obj for obj in objects if obj['kind'] == 'Deployment')
assert deployment['metadata']['name'] == 'dex'
mounts = deployment['spec']['template']['spec']['containers'][0]['volumeMounts']
assert any(mount['mountPath'].endswith('/light/styles.css') for mount in mounts)
secret = next(obj for obj in objects if obj['kind'] == 'Secret')
stored = yaml.safe_load(base64.b64decode(secret['data']['config.yaml']))
assert stored['storage']['config']['password'] == 'fixture-not-a-real-password'
assert not any(obj['kind'] in ['PersistentVolumeClaim', 'StatefulSet'] for obj in objects)
print(f'PASS: {len(objects)} real Helm resources; legacy identity/plain LDAP preserved, optional TLS, clients, theme mounts and Secret config validated')