TLS
This commit is contained in:
@@ -19,6 +19,9 @@ DEX_CLIENT_ID=www-membership
|
||||
DEX_CLIENT_SECRET=
|
||||
DEX_CONNECTOR_ID=www-ldap
|
||||
LDAP_URL=ldaps://ldap.eom.dev
|
||||
# Set to 1 ONLY with ldap:// on a trusted network to skip StartTLS.
|
||||
# Bind and registration passwords will be sent unencrypted. LDAPS stays verified.
|
||||
LDAP_ALLOW_INSECURE=0
|
||||
LDAP_BIND_DN=
|
||||
LDAP_BIND_PASSWORD=
|
||||
LDAP_CA_CERT_FILE=
|
||||
|
||||
@@ -11,7 +11,8 @@ Both payment methods feed the same LDAP group reconciliation and local expiry.
|
||||
1. Anyone can request a free account. A one-hour, single-use email link allows
|
||||
them to choose a password. The account is created as an `inetOrgPerson` in
|
||||
OpenLDAP; the password goes only through RFC 3062 Password Modify over
|
||||
certificate-verified LDAPS/StartTLS. This app never stores a password/hash.
|
||||
certificate-verified LDAPS/StartTLS by default (see the explicit plaintext
|
||||
option below). This app never stores a password/hash.
|
||||
2. Sign-in uses Dex Authorization Code + PKCE, state and nonce. Authlib verifies
|
||||
the RS256 ID token, issuer, audience, signature and lifetime. A signed
|
||||
`federated_claims` connector/user ID must match the configured LDAP connector
|
||||
@@ -89,7 +90,13 @@ of these five prices when inspected.
|
||||
tree**, including other account registrars. Only then set
|
||||
`LDAP_PASSWORD_SCHEME={ARGON2}`. The app does not alter slapd configuration.
|
||||
If a private CA is used, mount its certificate read-only into both membership
|
||||
and worker services and set `LDAP_CA_CERT_FILE`. TLS verification is mandatory.
|
||||
and worker services and set `LDAP_CA_CERT_FILE`. TLS verification is mandatory
|
||||
when TLS is used. To deliberately use plaintext on a trusted internal network,
|
||||
set `LDAP_URL=ldap://openldap.openldap.svc.cluster.local:389` and
|
||||
`LDAP_ALLOW_INSECURE=1` in both web and worker environments. This skips StartTLS
|
||||
and exposes bind passwords, registration passwords and directory data to the
|
||||
network. The default is `0` (StartTLS required for `ldap://`); TLS failures never
|
||||
trigger a plaintext fallback, and `ldaps://` always verifies certificates.
|
||||
4. Add a dedicated Dex client/LDAP connector using
|
||||
[membership/deploy/dex-membership.example.yaml](membership/deploy/dex-membership.example.yaml).
|
||||
The exact callback is `https://eom.dev/members/oidc/callback/` (trailing slash).
|
||||
|
||||
@@ -87,6 +87,8 @@ DEX_CLIENT_SECRET = os.environ.get("DEX_CLIENT_SECRET", "")
|
||||
DEX_CONNECTOR_ID = os.environ.get("DEX_CONNECTOR_ID", "ldap")
|
||||
|
||||
LDAP_URL = os.environ.get("LDAP_URL", "")
|
||||
# Explicit opt-in for trusted internal networks; never a TLS-error fallback.
|
||||
LDAP_ALLOW_INSECURE = os.environ.get("LDAP_ALLOW_INSECURE", "0") == "1"
|
||||
LDAP_BIND_DN = os.environ.get("LDAP_BIND_DN", "")
|
||||
LDAP_BIND_PASSWORD = os.environ.get("LDAP_BIND_PASSWORD", "")
|
||||
LDAP_CA_CERT_FILE = os.environ.get("LDAP_CA_CERT_FILE") or None
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""TLS-only LDAP; paid groups are never granted during registration or login."""
|
||||
"""TLS by default; paid groups are never granted during registration or login."""
|
||||
|
||||
import re
|
||||
import ssl
|
||||
@@ -22,7 +22,8 @@ def connection():
|
||||
conn = Connection(server, user=settings.LDAP_BIND_DN, password=settings.LDAP_BIND_PASSWORD, raise_exceptions=True, receive_timeout=8, auto_referrals=False)
|
||||
try:
|
||||
conn.open()
|
||||
if url.scheme == "ldap" and not conn.start_tls():
|
||||
# Plaintext requires explicit operator consent; LDAPS is never downgraded.
|
||||
if url.scheme == "ldap" and not settings.LDAP_ALLOW_INSECURE and not conn.start_tls():
|
||||
raise LDAPException("StartTLS failed; refusing to bind.")
|
||||
if not conn.bind():
|
||||
raise LDAPException("LDAP bind failed.")
|
||||
|
||||
@@ -471,7 +471,7 @@ class DirectoryTests(SimpleTestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
directory.identity_for_login("*)(uid=*)")
|
||||
|
||||
@override_settings(LDAP_URL="ldap://ldap.example.org", LDAP_BIND_DN="cn=service", LDAP_BIND_PASSWORD="test-only", LDAP_ACCOUNT_BASE_DN=BASE)
|
||||
@override_settings(LDAP_URL="ldap://ldap.example.org", LDAP_ALLOW_INSECURE=False, LDAP_BIND_DN="cn=service", LDAP_BIND_PASSWORD="test-only", LDAP_ACCOUNT_BASE_DN=BASE)
|
||||
@patch("members.directory.Connection")
|
||||
def test_starttls_failure_never_binds_credentials(self, conn_factory):
|
||||
conn_factory.return_value.start_tls.return_value = False
|
||||
@@ -480,3 +480,29 @@ class DirectoryTests(SimpleTestCase):
|
||||
pass
|
||||
conn_factory.return_value.bind.assert_not_called()
|
||||
conn_factory.return_value.unbind.assert_called_once()
|
||||
|
||||
@override_settings(LDAP_BIND_DN="cn=service", LDAP_BIND_PASSWORD="test-only", LDAP_ACCOUNT_BASE_DN=BASE, LDAP_CA_CERT_FILE=None)
|
||||
@patch("members.directory.Connection")
|
||||
@patch("members.directory.Server")
|
||||
def test_ldap_transport_modes(self, server_factory, conn_factory):
|
||||
import ssl
|
||||
|
||||
for scheme, insecure, starttls, port in (
|
||||
("ldap", False, True, 389),
|
||||
("ldap", True, False, 389),
|
||||
("ldaps", False, False, 636),
|
||||
("ldaps", True, False, 636),
|
||||
):
|
||||
with self.subTest(scheme=scheme, insecure=insecure):
|
||||
conn_factory.reset_mock()
|
||||
with override_settings(LDAP_URL=f"{scheme}://ldap.example.org", LDAP_ALLOW_INSECURE=insecure):
|
||||
with directory.connection() as conn:
|
||||
self.assertIs(conn, conn_factory.return_value)
|
||||
self.assertEqual(conn_factory.return_value.start_tls.call_count, int(starttls))
|
||||
conn_factory.return_value.open.assert_called_once()
|
||||
conn_factory.return_value.bind.assert_called_once()
|
||||
conn_factory.return_value.unbind.assert_called_once()
|
||||
options = server_factory.call_args.kwargs
|
||||
self.assertEqual(options["use_ssl"], scheme == "ldaps")
|
||||
self.assertEqual(options["port"], port)
|
||||
self.assertEqual(options["tls"].validate, ssl.CERT_REQUIRED)
|
||||
|
||||
Reference in New Issue
Block a user