Compare commits

...
5 Commits
Author SHA1 Message Date
claudeandClaude Opus 5.5 b444a6426b Dex: add listmonk client for the eom.dev newsletter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 20:19:53 -05:00
ericandClaude Opus 5.5 fa17989fed Dex: send LDAP groups, accept membership accounts, gate Mastodon
- Match groups by DN so the groups claim is populated
- Use cn for names; membership-created accounts have no givenName
- Add a mastodon connector that only admits Mastodon Users, and pin
  every client to its connector via allowedConnectors
- Register Nextcloud's user_oidc callback without index.php

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:54:59 -05:00
eric 642c2ed818 Read only file system 2026-10-27 16:42:21 -04:00
eric 0e62fb0962 Don't distinguish memberships and accounts 2026-10-27 16:04:34 -04:00
eric 9a31efc38e Deploy 2026-10-27 16:04:33 -04:00
7 changed files with 84 additions and 78 deletions
Vendored Executable
+9
View File
@@ -0,0 +1,9 @@
{
"python-envs.pythonProjects": [
{
"path": ".",
"envManager": "ms-python.python:system",
"packageManager": "ms-python.python:pip"
}
]
}
+19 -15
View File
@@ -59,12 +59,13 @@ is a separate app-side migration, not another set of automatically active OIDC
environment variables. Keep a tested administrator fallback and avoid automatic environment variables. Keep a tested administrator fallback and avoid automatic
email-only account linking when migrating existing users. email-only account linking when migrating existing users.
The website's `www-ldap` connector uses entryUUID while the legacy `ldap` The eom.dev deployment uses one `ldap` connector for legacy POSIX accounts and
connector uses uid. Those produce different subjects for the same directory website `inetOrgPerson` accounts. It retains `idAttr: uid`, so existing OIDC
entry: do not switch existing accounts between connectors without an explicit subjects remain stable. The website resolves the signed UID to LDAP and anchors
linking/migration plan. The legacy connector still filters for Dex Users; merely its account link to immutable `entryUUID`. Its combined user filter admits
registering an application does not bypass this restriction or grant paid room `inetOrgPerson` entries throughout the configured search base; review that
access. Each relying application must implement its own authorization policy. authentication scope before deployment. Each relying application must still
implement its own authorization policy.
Run the playbook on the controller with its kubeconfig and this role available 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` in `ANSIBLE_ROLES_PATH`. Load your encrypted variables with Ansible's `--extra-vars`
@@ -145,8 +146,9 @@ 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. 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 The generic connector uses `inetOrgPerson` and DN-based group matching. The site
playbook retains ID `ldap`, `idAttr: uid`, its POSIX/group filter, playbook retains ID `ldap`, `idAttr: uid`, its POSIX/Dex Users filter branch plus
and `userAttr: cn` group matching. For `uniqueMember` values containing full DNs, website `inetOrgPerson` accounts, and `userAttr: cn` group matching. For
`uniqueMember` values containing full DNs,
set `dex_ldap_group_user_attribute: DN` deliberately after testing. Changing set `dex_ldap_group_user_attribute: DN` deliberately after testing. Changing
connector ID or idAttr changes OIDC subjects and can detach existing accounts. connector ID or idAttr changes OIDC subjects and can detach existing accounts.
@@ -167,19 +169,21 @@ diff-suppressed. Dex configuration is stored by the official chart in a Kubernet
**Secret**, never in the theme ConfigMap. Helm stores release values, including **Secret**, never in the theme ConfigMap. Helm stores release values, including
inline credentials, in its release Secrets: protect these with RBAC, encryption inline credentials, in its release Secrets: protect these with RBAC, encryption
at rest and encrypted backups. This role does not generate or rotate passwords. at rest and encrypted backups. This role does not generate or rotate passwords.
The root filesystem stays read-only; a 16 MiB memory-backed `emptyDir` mounted
at `/tmp` provides Dex's required temporary config-file writes without making
the container filesystem writable.
Client `secretEnv` can refer to a key in `dex_env_secret_name`. LDAP connectors 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 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 database password remains a protected Helm/Dex config value; it is not assumed
to support the same connector substitution. Never commit actual secrets. to support the same connector substitution. Never commit actual secrets.
[tests/example-vars.yml](tests/example-vars.yml) shows a www client and separate [tests/example-vars.yml](tests/example-vars.yml) shows a www client using the
`www-ldap` connector using `entryUUID`, `inetOrgPerson` and DN group matching. single existing `ldap` connector. The Secret's `WWW_DEX_CLIENT_SECRET` must match
This avoids breaking legacy subjects while supporting new free website users. the website's `DEX_CLIENT_SECRET`; set `DEX_CONNECTOR_ID=ldap` and the exact
The Secret's `WWW_DEX_CLIENT_SECRET` must match the website's `DEX_CLIENT_SECRET`; matching issuer. No client or additional connector is silently enabled by
set the website's `DEX_CONNECTOR_ID=www-ldap` and exact matching issuer. No new default. Verify the combined search filter and scope before enabling public
client or connector is silently enabled by default. Verify that this connector's registration.
search base includes the website account OU before enabling public registration.
### Theme ### Theme
+1 -1
View File
@@ -52,7 +52,7 @@ dex_matrix_client_enabled: false
dex_matrix_redirect_uris: [] dex_matrix_redirect_uris: []
# Full objects (id, name, secret or secretEnv, redirectURIs). Use Vault for secrets. # Full objects (id, name, secret or secretEnv, redirectURIs). Use Vault for secrets.
dex_static_clients: [] dex_static_clients: []
# Additional connectors let www use entryUUID without changing legacy subjects. # Optional extra connectors; the eom.dev site uses its existing LDAP connector.
dex_additional_connectors: [] dex_additional_connectors: []
# Optional environment Secret for secretEnv and $ENV connector credentials. # Optional environment Secret for secretEnv and $ENV connector credentials.
dex_env_secret_name: "" dex_env_secret_name: ""
+44 -34
View File
@@ -4,7 +4,7 @@
- name: Deploy eom.dev Dex with Catppuccin branding - name: Deploy eom.dev Dex with Catppuccin branding
hosts: alpha-control-plane hosts: alpha-control-plane
vars_files: vars_files:
../secrets.yaml ~/Desktop/secrets.yaml
become: true become: true
vars: vars:
dex_namespace: dex dex_namespace: dex
@@ -31,7 +31,8 @@
dex_postgres_password: "{{ dex_admin_password }}" dex_postgres_password: "{{ dex_admin_password }}"
dex_postgres_admin_password: "{{ postgresql_admin_password | default('') }}" dex_postgres_admin_password: "{{ postgresql_admin_password | default('') }}"
# Explicitly preserve internal plaintext LDAP and legacy OIDC subjects. # Keep the existing connector ID and UID subject mapping. The filter also
# admits website inetOrgPerson accounts so there is one Dex login option.
dex_ldap_host: openldap.openldap.svc.cluster.local dex_ldap_host: openldap.openldap.svc.cluster.local
dex_ldap_insecure_no_ssl: true dex_ldap_insecure_no_ssl: true
dex_ldap_start_tls: false dex_ldap_start_tls: false
@@ -40,11 +41,11 @@
dex_ldap_connector_id: ldap dex_ldap_connector_id: ldap
dex_ldap_connector_name: eom.dev account dex_ldap_connector_name: eom.dev account
dex_ldap_user_base_dn: dc=eom,dc=dev 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_user_filter: "(|(&(objectClass=posixAccount)(memberOf=cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev))(objectClass=inetOrgPerson))"
dex_ldap_id_attribute: uid dex_ldap_id_attribute: uid
dex_ldap_name_attribute: givenName dex_ldap_name_attribute: cn
dex_ldap_group_base_dn: dc=eom,dc=dev dex_ldap_group_base_dn: dc=eom,dc=dev
dex_ldap_group_user_attribute: cn dex_ldap_group_user_attribute: DN
dex_ldap_group_filter: "(objectClass=groupOfUniqueNames)" dex_ldap_group_filter: "(objectClass=groupOfUniqueNames)"
# All clients are declared once below; don't also emit the convenience client. # All clients are declared once below; don't also emit the convenience client.
@@ -55,34 +56,41 @@
- id: "{{ matrix_dex_oidc_client_id }}" - id: "{{ matrix_dex_oidc_client_id }}"
name: Matrix Auth Service name: Matrix Auth Service
secret: "{{ matrix_dex_oidc_client_secret }}" secret: "{{ matrix_dex_oidc_client_secret }}"
allowedConnectors: [ldap]
redirectURIs: redirectURIs:
- https://mas.eom.dev/upstream/callback/01K96AQEZKKABW34PY3R6BVNJ4 - https://mas.eom.dev/upstream/callback/01K96AQEZKKABW34PY3R6BVNJ4
- id: www-membership - id: www-membership
name: eom.dev Membership name: eom.dev Membership
secret: "{{ www_dex_client_secret }}" secret: "{{ www_dex_client_secret }}"
allowedConnectors: [ldap]
redirectURIs: redirectURIs:
- https://www.eom.dev/members/oidc/callback/ - https://www.eom.dev/members/oidc/callback/
- id: gitea - id: gitea
name: Gitea name: Gitea
secret: "{{ gitea_dex_client_secret }}" secret: "{{ gitea_dex_client_secret }}"
# Authentication source in Gitea must be named exactly "dex". # Authentication source in Gitea must be named exactly "Dex".
allowedConnectors: [ldap]
redirectURIs: redirectURIs:
- https://gitea.eom.dev/user/oauth2/dex/callback - https://gitea.eom.dev/user/oauth2/Dex/callback
- id: discourse - id: discourse
name: Discourse name: Discourse
secret: "{{ discourse_dex_client_secret }}" secret: "{{ discourse_dex_client_secret }}"
# discourse-openid-connect strategy name is oidc. # discourse-openid-connect strategy name is oidc.
allowedConnectors: [ldap]
redirectURIs: redirectURIs:
- https://discourse.eom.dev/auth/oidc/callback - https://discourse.eom.dev/auth/oidc/callback
- id: peertube - id: peertube
name: PeerTube name: PeerTube
secret: "{{ peertube_dex_client_secret }}" secret: "{{ peertube_dex_client_secret }}"
# Requires peertube-plugin-auth-openid-connect. # Requires peertube-plugin-auth-openid-connect.
allowedConnectors: [ldap]
redirectURIs: redirectURIs:
- https://peertube.eom.dev/plugins/auth-openid-connect/router/code-cb - https://peertube.eom.dev/plugins/oidc-cnr/router/code-cb
- id: mastodon - id: mastodon
name: Mastodon name: Mastodon
secret: "{{ mastodon_dex_client_secret }}" secret: "{{ mastodon_dex_client_secret }}"
# Tier gate: only the connector that admits Mastodon Users.
allowedConnectors: [mastodon]
# Matches OIDC_REDIRECT_URI in the existing Mastodon deployment. # Matches OIDC_REDIRECT_URI in the existing Mastodon deployment.
redirectURIs: redirectURIs:
- https://mastodon.eom.dev/auth/auth/openid_connect/callback - https://mastodon.eom.dev/auth/auth/openid_connect/callback
@@ -91,33 +99,35 @@
secret: "{{ nextcloud_dex_client_secret }}" secret: "{{ nextcloud_dex_client_secret }}"
# This chooses the user_oidc app, not oidc_login or the OIDC provider app. # This chooses the user_oidc app, not oidc_login or the OIDC provider app.
# Override from its admin screen if your installation rewrites index.php. # Override from its admin screen if your installation rewrites index.php.
allowedConnectors: [ldap]
redirectURIs: redirectURIs:
- https://nextcloud.eom.dev/apps/user_oidc/code
- https://nextcloud.eom.dev/index.php/apps/user_oidc/code - https://nextcloud.eom.dev/index.php/apps/user_oidc/code
- id: listmonk
name: eom.dev Newsletter
secret: "{{ listmonk_dex_client_secret }}"
# listmonk only admits e-mails already present as listmonk users.
allowedConnectors: [ldap]
redirectURIs:
- https://listmonk.eom.dev/auth/oidc
# Per-service gate for clients that cannot check group claims themselves.
# Same idAttr as the main connector; subjects differ per connector ID.
dex_additional_connectors: dex_additional_connectors:
- "{{ _dex_eom_www_connector }}" - type: ldap
_dex_eom_www_connector: id: mastodon
type: ldap name: eom.dev account
id: www-ldap config:
name: eom.dev Membership host: openldap.openldap.svc.cluster.local
config: insecureNoSSL: true
host: openldap.openldap.svc.cluster.local bindDN: cn=readonly,dc=eom,dc=dev
insecureNoSSL: true bindPW: "{{ openldap_readonly_password }}"
bindDN: cn=readonly,dc=eom,dc=dev userSearch:
bindPW: "{{ openldap_readonly_password }}" baseDN: dc=eom,dc=dev
userSearch: filter: "(&(objectClass=inetOrgPerson)(memberOf=cn=Mastodon Users,ou=Mastodon,ou=Services,dc=eom,dc=dev))"
baseDN: ou=people,dc=eom,dc=dev username: uid
filter: "(objectClass=inetOrgPerson)" idAttr: uid
username: uid emailAttr: mail
idAttr: entryUUID nameAttr: cn
emailAttr: mail preferredUsernameAttr: uid
nameAttr: cn
preferredUsernameAttr: uid
groupSearch:
baseDN: ou=groups,dc=eom,dc=dev
filter: "(objectClass=groupOfUniqueNames)"
userMatchers:
- userAttr: DN
groupAttr: uniqueMember
nameAttr: cn
roles: roles:
- ansible-role-dex - roles/ericomeehan.dex
+2 -1
View File
@@ -84,11 +84,11 @@ config:
- {{ connector | to_json }} - {{ connector | to_json }}
{% endfor %} {% endfor %}
volumes: volumes:
# The official entrypoint renders its config into /tmp before starting Dex.
- name: dex-tmp - name: dex-tmp
emptyDir: emptyDir:
medium: Memory medium: Memory
sizeLimit: 16Mi sizeLimit: 16Mi
{% if dex_theme_enabled %} {% if dex_theme_enabled %}
- name: theme - name: theme
configMap: configMap:
@@ -105,6 +105,7 @@ volumes:
volumeMounts: volumeMounts:
- name: dex-tmp - name: dex-tmp
mountPath: /tmp mountPath: /tmp
{% if dex_theme_enabled %} {% if dex_theme_enabled %}
- name: theme - name: theme
mountPath: /srv/dex/web/themes/light/styles.css mountPath: /srv/dex/web/themes/light/styles.css
+6 -26
View File
@@ -20,29 +20,9 @@ dex_static_clients:
redirectURIs: redirectURIs:
- https://www.eom.dev/members/oidc/callback/ - https://www.eom.dev/members/oidc/callback/
# Additional connector: don't alter legacy ldap/idAttr uid, which changes sub # One LDAP connector admits both legacy POSIX accounts and website accounts.
# identifiers for existing applications. This connector admits free web accounts. # Keep its ID and idAttr unchanged so existing OIDC subjects remain stable.
dex_additional_connectors: dex_ldap_connector_id: ldap
- type: ldap dex_ldap_id_attribute: uid
id: www-ldap dex_ldap_user_base_dn: dc=eom,dc=dev
name: eom.dev Membership dex_ldap_user_filter: "(|(&(objectClass=posixAccount)(memberOf=cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev))(objectClass=inetOrgPerson))"
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
+3 -1
View File
@@ -31,6 +31,8 @@ for fixture_run in (1,):
assert resolved['dex_ldap_group_user_attribute'] == 'cn' assert resolved['dex_ldap_group_user_attribute'] == 'cn'
assert resolved['dex_theme_enabled'] is True assert resolved['dex_theme_enabled'] is True
assert resolved['dex_postgres_manage'] is False assert resolved['dex_postgres_manage'] is False
assert resolved['dex_additional_connectors'] == []
assert resolved['dex_ldap_user_filter'] == '(|(&(objectClass=posixAccount)(memberOf=cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev))(objectClass=inetOrgPerson))'
clients = {client['id']: client for client in resolved['dex_static_clients']} clients = {client['id']: client for client in resolved['dex_static_clients']}
assert len(clients) == len(resolved['dex_static_clients']) == 7 assert len(clients) == len(resolved['dex_static_clients']) == 7
assert resolved['dex_matrix_client_enabled'] is False assert resolved['dex_matrix_client_enabled'] is False
@@ -42,7 +44,6 @@ for fixture_run in (1,):
assert clients['mastodon']['redirectURIs'] == ['https://mastodon.eom.dev/auth/auth/openid_connect/callback'] assert clients['mastodon']['redirectURIs'] == ['https://mastodon.eom.dev/auth/auth/openid_connect/callback']
assert clients['nextcloud']['redirectURIs'] == ['https://nextcloud.eom.dev/index.php/apps/user_oidc/code'] assert clients['nextcloud']['redirectURIs'] == ['https://nextcloud.eom.dev/index.php/apps/user_oidc/code']
assert all(client['secret'] and len(client['redirectURIs']) == 1 for client in clients.values()) assert all(client['secret'] and len(client['redirectURIs']) == 1 for client in clients.values())
assert resolved['dex_additional_connectors'][0]['config']['userSearch']['idAttr'] == 'entryUUID'
values = yaml.safe_load(Path('/tmp/dex-values.yml').read_text()) values = yaml.safe_load(Path('/tmp/dex-values.yml').read_text())
config = values['config'] config = values['config']
@@ -53,6 +54,7 @@ ldap = config['connectors'][0]
assert ldap['id'] == 'ldap' assert ldap['id'] == 'ldap'
assert ldap['config']['insecureNoSSL'] is True assert ldap['config']['insecureNoSSL'] is True
assert ldap['config']['startTLS'] is False assert ldap['config']['startTLS'] is False
assert len(config['connectors']) == 1
assert ldap['config']['userSearch']['idAttr'] == 'uid' assert ldap['config']['userSearch']['idAttr'] == 'uid'
assert ldap['config']['groupSearch']['userMatchers'][0]['userAttr'] == 'DN' assert ldap['config']['groupSearch']['userMatchers'][0]['userAttr'] == 'DN'
assert config['staticClients'][0]['id'] == 'matrix-fixture' assert config['staticClients'][0]['id'] == 'matrix-fixture'