Explorar o código

HPCC-25489 LDAP credentials should be configurable as secrets

Add support to externalize LDAP and other credentials as Kubernetes secrets
and Hashicorp vault secrets. Added "authn" category to secrets and vault, and
modified ECLWatch ldap.yaml to allow specification of secret name. Added README.md
markdown to document LDAP configuration using k8s secrets, or
Hashicorp vault. Added helm schema for LDAP. Removed plain text password from
helm config, and provided a default k8s LDAP creds k/v pair

Signed-off-by: Russ Whitehead <william.whitehead@lexisnexisrisk.com>
Russ Whitehead %!s(int64=3) %!d(string=hai) anos
pai
achega
4f2aa38bbe

+ 4 - 8
esp/applications/common/ldap/azure_ldap.yaml

@@ -1,21 +1,15 @@
 ldap:
-  objname: ldapserver
   serverType: AzureActiveDirectory
   description: LDAP server process
-  ldapProtocol: ldap
-  localDomain: localdomain
+  ldapProtocol: ldaps
   ldapPort: 389
   ldapSecurePort: 636
-  systemCommonName: hpcc_admin2
-  systemPassword: ""
-  systemUser: hpcc_admin2
-  adminGroupName:
+  adminGroupName: HPCCAdmins
   maxConnections: 10
   passwordExpirationWarningDays: 10
   cacheTimeout: 5
   ldapTimeoutSecs: 131
   sharedCache: true
-  checkViewPermissions: ''
   filesBasedn: ou=files,ou=ecl
   groupsBasedn: ou=AADDC Users
   sudoersBasedn: ou=SUDOers
@@ -23,3 +17,5 @@ ldap:
   usersBasedn: ou=AADDC Users
   resourcesBasedn: ou=WsEcl,ou=EspServices,ou=ecl
   workunitsBasedn: ou=workunits,ou=ecl
+  ldapAdminSecretKey: "ldapadmincredskey"
+  ldapAdminVaultId: ""

+ 20 - 23
esp/applications/common/ldap/ldap.yaml

@@ -1,24 +1,21 @@
 ldap:
-   objname: ldapserver
-   description: LDAP server process
-   ldapProtocol: ldaps
-   localDomain: localdomain
-   ldapPort: 389
-   ldapSecurePort: 636
-   systemCommonName: hpcc_admin2
-   systemPassword: ""
-   systemUser: hpcc_admin2
-   adminGroupName: HPCCAdmin
-   maxConnections: 10
-   passwordExpirationWarningDays: 10
-   cacheTimeout: 5
-   ldapTimeoutSecs: 131
-   sharedCache: true
-   checkViewPermissions: ''
-   filesBasedn: ou=files,ou=ecl
-   groupsBasedn: ou=groups,ou=ecl
-   sudoersBasedn: ou=SUDOers
-   systemBasedn: cn=Users
-   usersBasedn: ou=users,ou=ecl
-   resourcesBasedn: ou=WsEcl,ou=EspServices,ou=ecl
-   workunitsBasedn: ou=workunits,ou=ecl
+  serverType: ActiveDirectory
+  description: LDAP server process
+  ldapProtocol: ldaps
+  ldapPort: 389
+  ldapSecurePort: 636
+  adminGroupName: HPCCAdmins
+  maxConnections: 10
+  passwordExpirationWarningDays: 10
+  cacheTimeout: 5
+  ldapTimeoutSecs: 131
+  sharedCache: true
+  filesBasedn: ou=files,ou=ecl
+  groupsBasedn: ou=groups,ou=ecl
+  sudoersBasedn: ou=SUDOers
+  systemBasedn: cn=Users
+  usersBasedn: ou=users,ou=ecl
+  resourcesBasedn: ou=WsEcl,ou=EspServices,ou=ecl
+  workunitsBasedn: ou=workunits,ou=ecl
+  ldapAdminSecretKey: "ldapadmincredskey"
+  ldapAdminVaultId: ""

+ 86 - 0
helm/examples/ldap/README.md

@@ -0,0 +1,86 @@
+# Containerized HPCC LDAP Support
+
+These examples demonstrate how to externalize HPCC LDAP Active Directory Security Manager administrator account credentials using Kubernetes and Hashicorp Vault secrets. To use externalized credentials, you should first run the tutorial on setting up Kubernetes secrets and the Hashicorp Vault, which can be found in the README.md file in the "HPCC-Platform\helm\examples\secrets" folder.
+
+Note that the LDAP Administrator account performs AD directory searches and modifications, and is the only HPCC user that must have Active Directory administrator rights.  This account should exist in the configured "systemBasedn" branch of the Active Directory, typically set to cn=Users.
+
+--------------------------------------------------------------------------------------------------------
+## Configure LDAP to use externalized Kubernetes (k8s) secrets
+
+### Create the k8s secret
+   From the CLI, create the LDAP "secret" similar to the following.
+   Make note of the secret name, "myk8sldapadmincreds" in this example.
+   The "username" and "password" key/values are required; additional properties are allowed but ignored.
+
+```bash
+   kubectl create secret generic myk8sldapadmincreds --from-literal=username=hpcc_admin --from-literal=password=t0pS3cr3tP@ssw0rd
+   kubectl get secret myk8sldapadmincreds
+```
+For more details on how to create secrets, see the "secrets" examples in the "HPCC-Platform\helm\examples\secrets" folder.
+
+### Deploy the k8s secret to the ECLWatch container
+   Modify the HPCC-Platform\helm\hpcc\values.yaml's "secrets:" category as follows.
+   Create a unique key name used to reference the secret, and set it to the secret value ("myk8sldapadmincreds") that you created above. In this example we give the key the name "ldapadminkey," and define an additional alternate one "ldapalternateadminkey" which could be used with another Active Directory server.
+   Note that the "ldapadminkey" key/value pair already exists as a default in the values.yaml file, and the key is referenced in the component's ldap.yaml file.  You may change these and add additional key/values as needed.
+
+```bash
+   secrets:
+     authn:
+       ldapadminkey: "myk8sldapadmincreds"
+       ldapalternateadminkey: "myk8sldapalternateadmincreds"
+```
+
+### Enable LDAP and reference the k8s secret key
+   In the HPCC-Platform\esp\applications\common\ldap\ldap.yaml (or azure_ldap.yaml) file, the "ldapAdminSecretKey" is already set to the key name created above. To enable LDAP authentication and to override this value if you don't want to use the default name, modify the ESP/ECLWatch helm component located in values.yaml as follows.
+
+```bash
+esp:
+- name: eclwatch
+  application: eclwatch
+  auth: ldap
+  ldapAddress: "127.0.0.1"
+  ldap:
+    ldapAdminSecretKey: "ldapadminkey"
+    servertype: "ActiveDirectory"
+```
+
+--------------------------------------------------------------------------------------------------------
+## Configure LDAP to use externalized Hashicorp Vault secrets
+
+### Create the vault secret
+   From the CLI, create the LDAP vault "secret" similar to the following.
+   Make note of the secret name, "myvaultadmincreds" in this example.
+   The "username" and "password" key/values are required, additional properties are allowed but ignored.
+   Make sure the secret name is specified with the "secret/authn/" prefix
+
+```bash
+   vault kv put secret/authn/myvaultadmincreds username=hpcc_admin password=t0pS3cr3tP@ssw0rd
+   vault kv get secret/authn/myvaultadmincreds
+```
+
+   For more details on how to create vault secrets, see the "secrets" examples in the "HPCC-Platform\helm\examples\secrets" folder.
+
+### Note that the vault name, my-authn-vault, was defined in the "secrets" tutorial, in the HPCC-Platform\helm\examples\secrets\values-secrets.yaml file as follows
+
+```bash
+  authn:
+    - name: my-authn-vault
+      #Note the data node in the URL is there for the REST APIs use. The path inside the vault starts after /data
+      url: http://${env.VAULT_SERVICE_HOST}:${env.VAULT_SERVICE_PORT}/v1/secret/data/authn/${secret}
+      kind: kv-v2
+```
+
+### Reference the secret key in the LDAP yaml or override
+   The key names "ldapAdminSecretKey" and "ldapAdminVaultId" are used by the LDAP security manager to resolve the secret, and must be spelled exactly as follows.
+
+```bash
+esp:
+- name: eclwatch
+  application: eclwatch
+  auth: ldap
+  ldapAddress: "127.0.0.1"
+  ldap:
+    ldapAdminSecretKey: "myvaultadmincreds"
+    ldapAdminVaultId: "my-authn-vault"
+    servertype: "ActiveDirectory"
+```

+ 4 - 0
helm/examples/secrets/hpcc_vault_policies.hcl

@@ -9,3 +9,7 @@ path "secret/data/ecl-user/*" {
 path "secret/data/storage/*" {
     capabilities = ["read"]
 }
+
+path "secret/data/authn/*" {
+    capabilities = ["read"]
+}

+ 5 - 1
helm/examples/secrets/values-secrets.yaml

@@ -17,7 +17,11 @@ vaults:
       #Note the data node in the URL is there for the REST APIs use. The path inside the vault starts after /data
       url: http://${env.VAULT_SERVICE_HOST}:${env.VAULT_SERVICE_PORT}/v1/secret/data/ecl/${secret}
       kind: kv-v2
-
+  authn:
+    - name: my-authn-vault
+      #Note the data node in the URL is there for the REST APIs use. The path inside the vault starts after /data
+      url: http://${env.VAULT_SERVICE_HOST}:${env.VAULT_SERVICE_PORT}/v1/secret/data/authn/${secret}
+      kind: kv-v2
     #vault using provided token auth
 #    - name: ecl-vault-direct-auth
        #Note the data node in the URL is there for the REST APIs use. The path inside the vault starts after /data

+ 1 - 1
helm/hpcc/templates/esp.yaml

@@ -73,7 +73,7 @@ data:
 {{- if not .disabled -}}
 {{- $env := concat ($.Values.global.env | default list) (.env | default list) -}}
 {{- $application := .application | default "eclwatch" -}}
-{{- $secretsCategories := ternary (list "storage" "esp" "codeSign" "codeVerify")  (list "storage" "esp") (eq $application "eclwatch") -}}
+{{- $secretsCategories := ternary (list "storage" "esp" "codeSign" "codeVerify" "authn")  (list "storage" "esp") (eq $application "eclwatch") -}}
 {{- $includeStorageCategories := ternary (list "lz" "data") (list "data") (eq $application "eclwatch") -}}
 {{- $commonCtx := dict "root" $ "me" . "secretsCategories" $secretsCategories  "includeCategories" $includeStorageCategories "env" $env -}}
 {{- $configSHA := include "hpcc.getConfigSHA" ($commonCtx | merge (dict "configMapHelper" "hpcc.espConfigMap" "component" "esp" "excludeKeys" "global,esp.queues")) -}}

+ 91 - 0
helm/hpcc/values.schema.json

@@ -65,6 +65,9 @@
         "storage": {
           "$ref": "#/definitions/secrets"
         },
+        "authn": {
+          "$ref": "#/definitions/secrets"
+        },
         "ecl": {
           "$ref": "#/definitions/secrets"
         },
@@ -90,6 +93,9 @@
         "storage": {
           "$ref": "#/definitions/vaultCategory"
         },
+        "authn": {
+          "$ref": "#/definitions/vaultCategory"
+        },
         "esp": {
           "$ref": "#/definitions/vaultCategory"
         },
@@ -559,6 +565,88 @@
         }
       }
     },
+    "ldap": {
+      "type": "object",
+      "additionalProperties": { "type": ["integer", "string", "boolean"] },
+      "properties": {
+        "ldapProtocol": {
+          "type": "string",
+          "description": "The protocol to use - standard \"LDAP\" or secure \"LDAPS\" over SSL"
+        },
+        "servertype": {
+          "type": "string",
+          "description": "LDAP Server Implementation Type (\"ActiveDirectory\", \"AzureActiveDirectory\")"
+        },
+        "description": {
+          "type": "string",
+          "description": "Description of this Active Directory Server component"
+        },
+        "adminGroupName": {
+          "type": "string",
+          "description": "The Active Directory group containing HPCC Administrators"
+        },
+        "filesBasedn": {
+          "type": "string",
+          "description": "The base distinguished name that should be used when looking up HPCC file scopes on the Active Directory server"
+        },
+        "groupsBasedn": {
+          "type": "string",
+          "description": "The base distinguished name that should be used when looking up HPCC groups on the Active Directory server"
+        },
+        "usersBasedn": {
+          "type": "string",
+          "description": "The base distinguished name that should be used when looking up HPCC users on the Active Directory server"
+        },
+        "systemBasedn": {
+          "type": "string",
+          "description": "The base distinguished name of the Active Directory systemUser"
+        },
+        "resourcesBasedn": {
+          "type": "string",
+          "description": "The base distinguished name that should be used when looking up HPCC feature resources on the Active Directory server"
+        },
+        "workunitsBasedn": {
+          "type": "string",
+          "description": "The base distinguished name that should be used when looking up workunit scopes on the Active Directory server"
+        },
+        "ldapAdminSecretKey": {
+          "type": "string",
+          "description": "The key name to be used to look up the Active Directory system administrator account Username/Password"
+        },
+        "ldapAdminVaultId": {
+          "type": "string",
+          "description": "The optional vault name to be used to look up the Active Directory system administrator account Username/Password, using ldapAdminSecretKey"
+        },
+        "ldapPort": {
+          "type": "integer",
+          "description": "The port of the nonsecure Active Directory server"
+        },
+        "ldapSecurePort": {
+          "type": "integer",
+          "description": "The secure port of the secure Active Directory server"
+        },
+        "maxConnections": {
+          "type": "integer",
+          "description": "The maximum number of concurrent LDAP connections to the Active Directory server (default 10)"
+        },
+        "passwordExpirationWarningDays": {
+          "type": "integer",
+          "description": "Within this time period, ECLWatch displays a warning about pending password expiration"
+        },
+        "cacheTimeout": {
+          "type": "integer",
+          "description": "Time in minutes after which the cached security information should be reloaded"
+        },
+        "ldapTimeoutSecs": {
+          "type": "integer",
+          "description": "The maximum number of seconds to wait for most Active Directory calls"
+        },
+        "sharedCache": {
+          "type": "boolean",
+          "description": "Use a single, shared LDAP cache"
+        }
+      }
+    },
     "logging": {
       "type": "object",
       "properties": {
@@ -785,6 +873,9 @@
           "type": "object",
           "additionalProperties": { "type": "string" }
         },
+        "ldap": {
+          "$ref": "#/definitions/ldap"
+        },
         "service": {
           "description": "Service properties",
           "type": "object",

+ 8 - 2
helm/hpcc/values.yaml

@@ -208,7 +208,7 @@ certificates:
 ## The secrets section contains a set of categories, each of which contain a list of secrets.  The categories determine which
 ## components have access to the secrets.
 ## For each secret:
-##   name is the name that is is accessed by within the platform
+##   name is the name that it is accessed by within the platform
 ##   secret is the name of the secret that should be published
 secrets:
   #timeout: 300 # timeout period for cached secrets.  Should be similar to the k8s refresh period.
@@ -220,6 +220,10 @@ secrets:
     ## For example, to set the secret associated with the azure storage account "mystorageaccount" use
     ##azure-mystorageaccount: storage-myazuresecret
 
+  authn:
+    ## Category to deploy authentication secrets to container, and to create a key name alias to reference those secrets
+    ldapadmincredskey: "myk8sldapadmincreds"  ## Default k/v for LDAP authentication secrets
+
   ecl: {}
     ## Category for secrets published to all components that run ecl
 
@@ -240,7 +244,7 @@ secrets:
 ## by system components and not exposed directly to ECL code.
 ##
 ## For each vault:
-##   name is the name that is is accessed by within the platform
+##   name is the name that it is accessed by within the platform
 ##   url is the url used to read a secret from the vault.
 ##   kind is the type of vault being accessed, or the protocol to use to access the secrets
 ##   client_secret a kubernetes level secret that contains the client_token used to retrive secrets.
@@ -249,6 +253,8 @@ secrets:
 vaults:
   storage:
 
+  authn:
+
   ecl:
 
   ecl-user:

+ 64 - 34
system/security/LdapSecurity/ldapconnection.cpp

@@ -30,6 +30,7 @@
 #include "dautils.hpp"
 #include "dasds.hpp"
 #include "workunit.hpp"
+#include "jsecrets.hpp"
 
 #include <map>
 #include <string>
@@ -351,6 +352,62 @@ public:
 
         m_timeout = cfg->getPropInt(".//@ldapTimeoutSecs", LDAPTIMEOUT);
 
+        //------------------------------------------------
+        //Get LDAP Admin account username (m_sysuser_commonname) and password (m_sysuser_password)
+        // Can be specified as
+        //  - Kubernetes secret : lookup key value ldapAdminSecretKey
+        //  - Vault secret : lookup key value ldapAdminVaultId, ldapAdminSecretKey
+        //  - Hardcoded : systemCommonName, systemPassword (legacy environment.xml)
+        //------------------------------------------------
+
+        StringBuffer systemUserSecretKey;
+        cfg->getProp(".//@ldapAdminSecretKey", systemUserSecretKey);//vault/secrets LDAP username key
+        if (!systemUserSecretKey.isEmpty())
+        {
+            StringBuffer vaultId;
+            cfg->getProp(".//@ldapAdminVaultId", vaultId);//optional HashiCorp vault ID
+
+            DBGLOG("Retrieving LDAP Admin username/password from secrets repo: %s %s", !vaultId.isEmpty() ? vaultId.str() : "", systemUserSecretKey.str());
+            Owned<IPropertyTree> secretTree;
+            if (!isEmptyString(vaultId.str()))
+                secretTree.setown(getVaultSecret("authn", vaultId, systemUserSecretKey.str(), nullptr));
+            else
+                secretTree.setown(getSecret("authn", systemUserSecretKey.str()));
+            if (!secretTree)
+                throw MakeStringException(-1, "Error retrieving LDAP Admin username/password");
+
+            getSecretKeyValue(m_sysuser_commonname, secretTree, "username");
+            getSecretKeyValue(m_sysuser_password, secretTree, "password");
+
+            if (m_sysuser_commonname.isEmpty() || m_sysuser_password.isEmpty())
+            {
+                throw MakeStringException(-1, "Error extracting LDAP Admin username/password");
+            }
+            m_sysuser.set(m_sysuser_commonname);
+        }
+        else
+        {
+            //Legacy component config from environment.xml configuration
+            cfg->getProp(".//@systemCommonName", m_sysuser_commonname);
+            if (m_sysuser_commonname.isEmpty())
+                throw MakeStringException(-1, "systemCommonName is empty");
+
+            StringBuffer pwd;
+            cfg->getProp(".//@systemPassword", pwd);
+            if (pwd.isEmpty())
+                throw MakeStringException(-1, "systemPassword is empty");
+            decrypt(m_sysuser_password, pwd.str());//MD5 encrypted in config
+        }
+
+        StringBuffer sysBasedn;
+        cfg->getProp(".//@systemBasedn", sysBasedn);
+        if (sysBasedn.isEmpty())
+            throw MakeStringException(-1, "systemBasedn is empty");
+
+        //----------------------------------------------------
+        //Ensure at least one specified LDAP host is available
+        //----------------------------------------------------
+
         int rc = LDAP_OTHER;
         StringBuffer hostbuf, dcbuf;
         const char * ldapDomain = cfg->queryProp(".//@ldapDomain");
@@ -358,32 +415,14 @@ public:
         {
             getLdapHost(hostbuf);
             unsigned port = strieq("ldaps",m_protocol) ? m_ldap_secure_port : m_ldapport;
-            StringBuffer sysUserDN, decPwd;
-
-            {
-                StringBuffer pwd;
-                cfg->getProp(".//@systemPassword", pwd);
-                if (pwd.isEmpty())
-                    throw MakeStringException(-1, "systemPassword is empty");
-                decrypt(decPwd, pwd.str());
-
-                StringBuffer sysUserCN;
-                cfg->getProp(".//@systemCommonName", sysUserCN);
-                if (sysUserCN.isEmpty())
-                    throw MakeStringException(-1, "systemCommonName is empty");
 
-                StringBuffer sysBasedn;
-                cfg->getProp(".//@systemBasedn", sysBasedn);
-                if (sysBasedn.isEmpty())
-                    throw MakeStringException(-1, "systemBasedn is empty");
-
-                //Guesstimate system user baseDN based on config settings. It will be used if anonymous bind fails
-                sysUserDN.append("cn=").append(sysUserCN.str()).append(",").append(sysBasedn.str());
-            }
+            //Guesstimate system user baseDN based on config settings. It will be used if anonymous bind fails
+            StringBuffer sysUserDN;
+            sysUserDN.append("cn=").append(m_sysuser_commonname.str()).append(",").append(sysBasedn.str());
 
             for(int retries = 0; retries <= LDAPSEC_MAX_RETRIES; retries++)
             {
-                rc = LdapUtils::getServerInfo(hostbuf.str(), sysUserDN.str(), decPwd.str(), m_protocol, port, dcbuf, m_serverType, ldapDomain, m_timeout);
+                rc = LdapUtils::getServerInfo(hostbuf.str(), sysUserDN.str(), m_sysuser_password.str(), m_protocol, port, dcbuf, m_serverType, ldapDomain, m_timeout);
                 if(!LdapServerDown(rc) || retries >= LDAPSEC_MAX_RETRIES)
                     break;
                 sleep(LDAPSEC_RETRY_WAIT);
@@ -511,22 +550,13 @@ public:
         }
 
         m_sysuser_specified = true;
-        cfg->getProp(".//@systemUser", m_sysuser);
         if(m_sysuser.length() == 0)
         {
-            m_sysuser_specified = false;
+            cfg->getProp(".//@systemUser", m_sysuser);
+            if(m_sysuser.length() == 0)
+                m_sysuser_specified = false;
         }
 
-        cfg->getProp(".//@systemCommonName", m_sysuser_commonname);
-        if(m_sysuser_specified && (m_sysuser_commonname.length() == 0))
-        {
-            throw MakeStringException(-1, "SystemUser commonname is empty");
-        }
-
-        StringBuffer passbuf;
-        cfg->getProp(".//@systemPassword", passbuf);
-        decrypt(m_sysuser_password, passbuf.str());
-
         StringBuffer sysuser_basedn;
         cfg->getProp(".//@systemBasedn", sysuser_basedn);