|
| 1 | + |
| 2 | +# User Database support (LDAP) - POC |
| 3 | + |
| 4 | + |
| 5 | +## Goal |
| 6 | + |
| 7 | +We propose adding support for administrators who maintain an existing LDAP-compatible user directory, enabling them to reuse their current username and password credentials to obtain temporary access tokens for S3 operations. |
| 8 | +This feature will leverage the AWS STS operation AssumeRoleWithWebIdentity. In the proposed workflow: |
| 9 | +1. The client sends a signed(with a predfined constant signature) or unsigned JWT as the web identity token. |
| 10 | +2. The system parses the JWT to extract the LDAP username and password. |
| 11 | +3. These credentials are validated against a configured external LDAP server. |
| 12 | +4. Upon successful authentication, the system issues a temporary STS token, granting the user access to S3 resources for a limited duration. |
| 13 | + |
| 14 | +This approach ensures secure integration with existing identity infrastructures while eliminating the need to store or manage separate S3 credentials for LDAP users. |
| 15 | + |
| 16 | +## Configuring the external LDAP |
| 17 | +The administrator must store the LDAP configuration in the following file: |
| 18 | +/etc/noobaa-server/ldap_config |
| 19 | + |
| 20 | +The configuration should include: |
| 21 | + |
| 22 | +uri (Required) – The FQDN of the external LDAP server, in the format: |
| 23 | +ldaps://[server-ip-or-hostname]:[port] (e.g., port 636 for LDAPS) |
| 24 | + |
| 25 | +admin (Required) – An administrator username with permission to execute search queries on the LDAP server. |
| 26 | + |
| 27 | +secret (Required) – The password for the administrator account. |
| 28 | + |
| 29 | +search_dn (Required) – The distinguished name (DN) under which search queries will be performed. |
| 30 | + |
| 31 | +dn_attribute (Optional) – The DN attribute to be used in search queries (default: uid). |
| 32 | + |
| 33 | +search_scope (Optional) – Determines how deep the LDAP search should go from the search_dn (default: sub): |
| 34 | + |
| 35 | +* base – Search only the entry specified by search_dn. |
| 36 | + |
| 37 | +* one – Search immediate children of search_dn, but not deeper levels. |
| 38 | + |
| 39 | +* sub – Search the base DN and all its descendants recursively. |
| 40 | + |
| 41 | +jwt_secret (Optional) - The JWT secret the administrator will use to sign the token sent in AssumeRoleWithWebIdentity requests (default: unsigned). Once this option is set - unsigned tokens or tokens signed with different secret will be dropped as access denied. |
| 42 | + |
| 43 | +for example: |
| 44 | +```json |
| 45 | +{ |
| 46 | + "uri": "ldap://ldap.example.com:636", |
| 47 | + "admin": "cn=admin,dc=example,dc=com", |
| 48 | + "secret": "SuperSecurePassword123", |
| 49 | + "search_dn": "ou=users,dc=example,dc=com", |
| 50 | + "dn_attribute": "uid", |
| 51 | + "search_scope": "sub", |
| 52 | + "jwt_secret": "IAMTHEADMIN123!(SHOULDBE256BITS)" |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +## Sending an AssumeRoleWithWebIdentity request |
| 57 | +First create the json to be decoded: |
| 58 | +```json |
| 59 | +{ |
| 60 | + "user": "TheUserName", |
| 61 | + "password": "TheUserPassword", |
| 62 | + "type": "ldap" |
| 63 | +} |
| 64 | +``` |
| 65 | +Sign it |
| 66 | +node.js example: |
| 67 | +```js |
| 68 | +const jwt = require('jsonwebtoken'); |
| 69 | +console.log(jwt.sign({ user: "TheUserName", password: "TheUserPassword", type: "ldap" }, "IAMTHEADMIN123!(SHOULDBE256BITS)")); |
| 70 | +eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgxOTE3fQ. |
| 71 | +``` |
| 72 | +Or you can use 'none' algorithm if you are not interested with verifying the token |
| 73 | +```js |
| 74 | +const jwt = require('jsonwebtoken'); |
| 75 | +console.log(jwt.sign({ user: "TheUserName", password: "TheUserPassword", type: "ldap" }, undefined, { algorithm: 'none' })); |
| 76 | +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgyMzQ2fQ.P6WYcdM0kJagNK4D0M8AHiGFcUZ-DhTOKHlC1-AxcT0 |
| 77 | +``` |
| 78 | +Now use this token with AWS STS AssumeRoleWithWebIdentity: |
| 79 | +```bash |
| 80 | +aws sts assume-role-with-web-identity --endpoint [endpoint] --role-arn [role-arn] --role-session-name [session-name] --web-identity-token eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgxOTE3fQ. |
| 81 | +``` |
| 82 | +role-name - ARN of the role that the caller is assuming. Make sure you created this role in advance using account-api or noobaa-cli (See Appendix. A). Format is: `arn:aws:sts::[user-access-key]:role/[role-name]` |
| 83 | + |
| 84 | +user-access-key - the access key of the account (temp secret key and token will be returned by the call) |
| 85 | + |
| 86 | +session-name - An identifier for the assumed role session. Typically, you pass the name or identifier that is associated with the user who is using your application. |
| 87 | + |
| 88 | +Output: The temporary security credentials, which include an access key ID, a secret access key, and a security token. |
| 89 | + |
| 90 | +read more here: https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html |
| 91 | + |
| 92 | +## Next steps |
| 93 | + |
| 94 | +1. See if we can move to using C open-ldap client as part of our native code instead of ldapts (for better performance and fewer security issues). see here: https://www.openldap.org/software/repo.html (We will mainly need bind and search) |
| 95 | +2. Add a system test that will create an external ldap and will check the full authentication flow. You can base on dockers I used for testing: |
| 96 | +* AD image: `docker run --rm --privileged -p 636:636 quay.io/samba.org/samba-ad-server:latest` (https://github.com/samba-in-kubernetes/samba-container) |
| 97 | +* LDAP image: `docker run --rm --privileged -p 636:636 ghcr.io/ldapjs/docker-test-openldap/openldap:latest` (https://github.com/ldapjs/docker-test-openldap/pkgs/container/docker-test-openldap%2Fopenldap) |
| 98 | +3. Add support to the operator side: |
| 99 | +* CLI command for configuring external LDAP |
| 100 | +* Create K8s Secret for LDAP info and mount to /etc/noobaa-server/ldap_config to the relevant pods |
| 101 | +4. Better align and adapt to the IAM effort also in POC stage |
| 102 | +5. See if we want to support encrypted password as part of the JWT token. see here: https://auth0.com/docs/secure/tokens/access-tokens/json-web-encryption |
| 103 | +6. We should maybe move ldap authentication to the authentication scope if possible |
| 104 | + |
| 105 | +## Appendix A: Creating account w/ role config using NooBaa API: |
| 106 | +```bash |
| 107 | +curl http://127.0.0.1:5001/rpc/ -sd '{ |
| 108 | + "api": "account_api", |
| 109 | + "method": "create_account", |
| 110 | + "params": { |
| 111 | + "name": "ldap", |
| 112 | + "email": "ldap", |
| 113 | + "has_login": false, |
| 114 | + "s3_access": true, |
| 115 | + "role_config": |
| 116 | + { |
| 117 | + "role_name": "ldap_user", |
| 118 | + "assume_role_policy": |
| 119 | + { |
| 120 | + "statement": [ |
| 121 | + { |
| 122 | + "effect": "allow", |
| 123 | + "action": ["sts:*"], |
| 124 | + "principal": ["*"] |
| 125 | + }] |
| 126 | + } |
| 127 | + } |
| 128 | + }, |
| 129 | + "auth_token": "'$(cat .nbtoken)'" |
| 130 | +}' |
| 131 | +``` |
| 132 | +in order to assume this role: |
| 133 | +```bash |
| 134 | +aws sts assume-role-with-web-identity --endpoint https://127.0.0.1:7443 --role-arn arn:aws:sts::pQII1cm5kFmpwqP6bzJh:role/ldap_user --role-session-name fry1 --web-identity-token eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiZnJ5IiwicGFzc3dvcmQiOiJmcnkiLCJ0eXBlIjoibGRhcCIsImlhdCI6MTc1NTQzMzkxOX0. --no-verify-ssl |
| 135 | +{ |
| 136 | + "Credentials": { |
| 137 | + "AccessKeyId": "vTFcHUuPqjKdtzTsMULP", |
| 138 | + "SecretAccessKey": "qBQxFcx99JOlABkZNq2fNRj6qGe18F9IREcHf9DZ", |
| 139 | + "SessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3Nfa2V5IjoidlRGY0hVdVBxaktkdHpUc01VTFAiLCJzZWNyZXRfa2V5IjoicUJReEZjeDk5Sk9sQUJrWk5xMmZOUmo2cUdlMThGOUlSRWNIZjlEWiIsImFzc3VtZWRfcm9sZV9hY2Nlc3Nfa2V5IjoicFFJSTFjbTVrRm1wd3FQNmJ6SmgiLCJpYXQiOjE3NTU0MzU3OTgsImV4cCI6MTc1NTQzOTM5OH0.z5Uap_7IPAbWCJyZC5zj8JleNshGxsYuhdbI9hOjr78", |
| 140 | + "Expiration": "2025-08-17T14:03:18+00:00" |
| 141 | + }, |
| 142 | + "AssumedRoleUser": { |
| 143 | + "AssumedRoleId": "pQII1cm5kFmpwqP6bzJh:fry1", |
| 144 | + "Arn": "arn:aws:sts::pQII1cm5kFmpwqP6bzJh:assumed-role/ldap_user/fry1" |
| 145 | + }, |
| 146 | + "SourceIdentity": "cn=Philip J. Fry,ou=people,dc=planetexpress,dc=com" |
| 147 | +} |
| 148 | +``` |
| 149 | + |
0 commit comments