|
| 1 | +# LDAP TLS Certificate Configuration |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This document describes the changes made to support configurable LDAP TLS certificate verification via the `LDAP_TLS_REQUIRE_CERT` environment variable. |
| 6 | + |
| 7 | +## Problem |
| 8 | + |
| 9 | +The application was hardcoded to use `ldap.OPT_X_TLS_NEVER` for TLS certificate verification, which meant it would never verify SSL/TLS certificates when connecting to LDAP servers. This was not configurable and could cause issues in different environments. |
| 10 | + |
| 11 | +## Solution |
| 12 | + |
| 13 | +Made the LDAP TLS certificate verification configurable via the `LDAP_TLS_REQUIRE_CERT` environment variable, allowing different verification levels based on deployment needs. |
| 14 | + |
| 15 | +## Environment Variable |
| 16 | + |
| 17 | +### `LDAP_TLS_REQUIRE_CERT` |
| 18 | + |
| 19 | +Controls how strictly the LDAP client verifies SSL/TLS certificates. |
| 20 | + |
| 21 | +**Possible Values:** |
| 22 | +- `never` - Don't require or verify certificates (use for self-signed certs or testing) |
| 23 | +- `allow` - Allow connection without certificate verification |
| 24 | +- `try` - Try to verify but proceed if verification fails |
| 25 | +- `demand` (default) - Require valid certificate (most secure) |
| 26 | +- `hard` - Same as `demand` |
| 27 | + |
| 28 | +**Example:** |
| 29 | +```bash |
| 30 | +export LDAP_TLS_REQUIRE_CERT=never |
| 31 | +``` |
| 32 | + |
| 33 | +Or in Kubernetes: |
| 34 | +```bash |
| 35 | +kubectl set env deployment/form-workflows-app -n default LDAP_TLS_REQUIRE_CERT=never |
| 36 | +``` |
| 37 | + |
| 38 | +## Files Modified |
| 39 | + |
| 40 | +### 1. `django_forms_workflows/ldap_backend.py` |
| 41 | + |
| 42 | +**Changes:** |
| 43 | +- Added `configure_ldap_connection(conn)` helper function that reads `LDAP_TLS_REQUIRE_CERT` environment variable and configures the LDAP connection accordingly |
| 44 | +- Updated three LDAP connection initialization points to use the helper: |
| 45 | + - `get_user_manager()` function (line 281) |
| 46 | + - `search_ldap_users()` function (line 359) |
| 47 | + - `get_ldap_user_attributes()` function (line 443) |
| 48 | + |
| 49 | +**Key Function:** |
| 50 | +```python |
| 51 | +def configure_ldap_connection(conn): |
| 52 | + """ |
| 53 | + Configure LDAP connection with TLS settings from environment variables. |
| 54 | + |
| 55 | + Environment Variables: |
| 56 | + LDAP_TLS_REQUIRE_CERT: TLS certificate verification level |
| 57 | + - 'never': Don't require or verify certificates |
| 58 | + - 'allow': Allow connection without cert verification |
| 59 | + - 'try': Try to verify but proceed if verification fails |
| 60 | + - 'demand' or 'hard': Require valid certificate (default) |
| 61 | + """ |
| 62 | + tls_require_cert = os.getenv('LDAP_TLS_REQUIRE_CERT', 'demand').lower() |
| 63 | + |
| 64 | + if tls_require_cert == 'never': |
| 65 | + conn.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) |
| 66 | + elif tls_require_cert == 'allow': |
| 67 | + conn.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW) |
| 68 | + elif tls_require_cert == 'try': |
| 69 | + conn.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_TRY) |
| 70 | + else: # 'demand' or 'hard' or any other value |
| 71 | + conn.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND) |
| 72 | + |
| 73 | + conn.set_option(ldap.OPT_REFERRALS, 0) |
| 74 | +``` |
| 75 | + |
| 76 | +### 2. `django_forms_workflows/handlers/ldap_handler.py` |
| 77 | + |
| 78 | +**Changes:** |
| 79 | +- Added `_configure_ldap_connection(conn)` helper function that imports and uses the main `configure_ldap_connection` function from `ldap_backend` |
| 80 | +- Includes fallback implementation if `ldap_backend` is not available |
| 81 | +- Updated LDAP connection initialization in `_update_ldap_entry()` method (line 217) |
| 82 | + |
| 83 | +### 3. `form-workflows/config/settings.py` |
| 84 | + |
| 85 | +**Changes:** |
| 86 | +- Modified the LDAP configuration section to read `LDAP_TLS_REQUIRE_CERT` from environment using `config()` function |
| 87 | +- Dynamically sets `tls_cert_option` based on the environment variable value |
| 88 | +- Updated `AUTH_LDAP_CONNECTION_OPTIONS` to use the dynamic `tls_cert_option` instead of hardcoded `ldap.OPT_X_TLS_NEVER` |
| 89 | + |
| 90 | +**Before:** |
| 91 | +```python |
| 92 | +AUTH_LDAP_CONNECTION_OPTIONS = { |
| 93 | + ldap.OPT_DEBUG_LEVEL: 0, |
| 94 | + ldap.OPT_REFERRALS: 0, |
| 95 | + ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER, # Hardcoded |
| 96 | + ldap.OPT_NETWORK_TIMEOUT: 5, |
| 97 | + ldap.OPT_TIMEOUT: 5, |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +**After:** |
| 102 | +```python |
| 103 | +# Configure TLS certificate verification based on environment variable |
| 104 | +tls_require_cert = config('LDAP_TLS_REQUIRE_CERT', default='demand').lower() |
| 105 | + |
| 106 | +if tls_require_cert == 'never': |
| 107 | + tls_cert_option = ldap.OPT_X_TLS_NEVER |
| 108 | +elif tls_require_cert == 'allow': |
| 109 | + tls_cert_option = ldap.OPT_X_TLS_ALLOW |
| 110 | +elif tls_require_cert == 'try': |
| 111 | + tls_cert_option = ldap.OPT_X_TLS_TRY |
| 112 | +else: # 'demand' or 'hard' or any other value |
| 113 | + tls_cert_option = ldap.OPT_X_TLS_DEMAND |
| 114 | + |
| 115 | +AUTH_LDAP_CONNECTION_OPTIONS = { |
| 116 | + ldap.OPT_DEBUG_LEVEL: 0, |
| 117 | + ldap.OPT_REFERRALS: 0, |
| 118 | + ldap.OPT_X_TLS_REQUIRE_CERT: tls_cert_option, # Configurable |
| 119 | + ldap.OPT_NETWORK_TIMEOUT: 5, |
| 120 | + ldap.OPT_TIMEOUT: 5, |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +### 4. `form-workflows/test_ldap_connection.py` |
| 125 | + |
| 126 | +**Changes:** |
| 127 | +- Updated the test script to read `LDAP_TLS_REQUIRE_CERT` from environment |
| 128 | +- Applies the same TLS configuration logic as the main application |
| 129 | +- Displays the TLS verification level being used during testing |
| 130 | + |
| 131 | +## Usage |
| 132 | + |
| 133 | +### For Development/Testing (Self-Signed Certificates) |
| 134 | + |
| 135 | +```bash |
| 136 | +export LDAP_TLS_REQUIRE_CERT=never |
| 137 | +python manage.py runserver |
| 138 | +``` |
| 139 | + |
| 140 | +### For Production (Valid Certificates) |
| 141 | + |
| 142 | +```bash |
| 143 | +export LDAP_TLS_REQUIRE_CERT=demand |
| 144 | +python manage.py runserver |
| 145 | +``` |
| 146 | + |
| 147 | +### For Kubernetes Deployment |
| 148 | + |
| 149 | +Update the deployment environment variables: |
| 150 | + |
| 151 | +```bash |
| 152 | +kubectl set env deployment/form-workflows-app -n default LDAP_TLS_REQUIRE_CERT=never |
| 153 | +``` |
| 154 | + |
| 155 | +Or add to the deployment YAML: |
| 156 | + |
| 157 | +```yaml |
| 158 | +env: |
| 159 | + - name: LDAP_TLS_REQUIRE_CERT |
| 160 | + value: "never" |
| 161 | +``` |
| 162 | +
|
| 163 | +## Testing |
| 164 | +
|
| 165 | +After making these changes, you can test LDAP connectivity: |
| 166 | +
|
| 167 | +```bash |
| 168 | +cd form-workflows |
| 169 | +python test_ldap_connection.py |
| 170 | +``` |
| 171 | + |
| 172 | +The test script will show which TLS verification level is being used. |
| 173 | + |
| 174 | +## Security Considerations |
| 175 | + |
| 176 | +- **Production**: Use `demand` (default) to ensure certificate verification |
| 177 | +- **Development/Testing**: Use `never` or `allow` for self-signed certificates |
| 178 | +- **Staging**: Use `try` to attempt verification but allow fallback |
| 179 | + |
| 180 | +## Backward Compatibility |
| 181 | + |
| 182 | +The default value is `demand`, which is the most secure option. However, if you were previously using the hardcoded `never` setting, you'll need to explicitly set: |
| 183 | + |
| 184 | +```bash |
| 185 | +export LDAP_TLS_REQUIRE_CERT=never |
| 186 | +``` |
| 187 | + |
| 188 | +## Next Steps |
| 189 | + |
| 190 | +1. **Rebuild the application** with these changes |
| 191 | +2. **Set the environment variable** in your deployment |
| 192 | +3. **Redeploy** the application |
| 193 | +4. **Test** LDAP connectivity |
| 194 | + |
| 195 | +### For form-workflows Deployment |
| 196 | + |
| 197 | +```bash |
| 198 | +# Navigate to form-workflows directory |
| 199 | +cd form-workflows |
| 200 | + |
| 201 | +# Rebuild the Docker image |
| 202 | +docker build -t form-workflows:latest . |
| 203 | + |
| 204 | +# Update Kubernetes deployment |
| 205 | +kubectl set env deployment/form-workflows-app -n default LDAP_TLS_REQUIRE_CERT=never |
| 206 | + |
| 207 | +# Restart the deployment to pick up code changes |
| 208 | +kubectl rollout restart deployment/form-workflows-app -n default |
| 209 | + |
| 210 | +# Monitor the rollout |
| 211 | +kubectl rollout status deployment/form-workflows-app -n default |
| 212 | +``` |
| 213 | + |
| 214 | +## Troubleshooting |
| 215 | + |
| 216 | +### Issue: LDAP connection still fails with certificate errors |
| 217 | + |
| 218 | +**Solution:** Verify the environment variable is set correctly: |
| 219 | +```bash |
| 220 | +kubectl exec -it deployment/form-workflows-app -- env | grep LDAP_TLS_REQUIRE_CERT |
| 221 | +``` |
| 222 | + |
| 223 | +### Issue: Environment variable not being read |
| 224 | + |
| 225 | +**Solution:** Ensure the deployment has been restarted after setting the environment variable: |
| 226 | +```bash |
| 227 | +kubectl rollout restart deployment/form-workflows-app -n default |
| 228 | +``` |
| 229 | + |
| 230 | +### Issue: Still getting SSL errors |
| 231 | + |
| 232 | +**Solution:** Try using the non-SSL LDAP URL instead: |
| 233 | +```bash |
| 234 | +kubectl set env deployment/form-workflows-app -n default LDAP_PRIMARY_URL=ldap://your-server:389 |
| 235 | +``` |
| 236 | + |
| 237 | +## References |
| 238 | + |
| 239 | +- [python-ldap Documentation](https://www.python-ldap.org/en/latest/) |
| 240 | +- [django-auth-ldap Documentation](https://django-auth-ldap.readthedocs.io/) |
| 241 | +- [OpenLDAP TLS Configuration](https://www.openldap.org/doc/admin24/tls.html) |
| 242 | + |
0 commit comments