diff --git a/server/build.gradle b/server/build.gradle index 8fa9ec8aee..7060fa5f09 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -20,6 +20,18 @@ buildscript { repositories { mavenCentral() + maven { + url "https://rt.adsw.io/artifactory/maven-arenadata-release/" + mavenContent { + releasesOnly() + } + } + maven { + url "https://rt.adsw.io/artifactory/maven-arenadata-snapshot/" + mavenContent { + snapshotsOnly() + } + } } } @@ -40,6 +52,18 @@ allprojects { repositories { mavenCentral() + maven { + url "https://rt.adsw.io/artifactory/maven-arenadata-release/" + mavenContent { + releasesOnly() + } + } + maven { + url "https://rt.adsw.io/artifactory/maven-arenadata-snapshot/" + mavenContent { + snapshotsOnly() + } + } } } @@ -114,6 +138,9 @@ configure(javaProjects) { dependency("org.wildfly.openssl:wildfly-openssl:1.0.7.Final") dependency("org.xerial.snappy:snappy-java:1.1.8.4") + // Arenadata encryption + dependency("io.arenadata.security:encryption:1.0.0") + // Hadoop dependencies dependencySet(group:"org.apache.hadoop", version:"${hadoopVersion}") { entry("hadoop-annotations") @@ -199,7 +226,6 @@ configure(javaProjects) { entry("aws-java-sdk-kms") entry("aws-java-sdk-s3") } - } } diff --git a/server/pxf-api/build.gradle b/server/pxf-api/build.gradle index 7809b8d96f..610dccfb43 100644 --- a/server/pxf-api/build.gradle +++ b/server/pxf-api/build.gradle @@ -39,6 +39,7 @@ dependencies { implementation("commons-configuration:commons-configuration") implementation("commons-lang:commons-lang") implementation("org.apache.commons:commons-lang3") + implementation("io.arenadata.security:encryption") implementation("org.apache.hadoop:hadoop-auth") { transitive = false } implementation("org.codehaus.woodstox:stax2-api") { transitive = false } diff --git a/server/pxf-api/src/main/java/org/greenplum/pxf/api/configuration/PxfJksTextEncryptorConfiguration.java b/server/pxf-api/src/main/java/org/greenplum/pxf/api/configuration/PxfJksTextEncryptorConfiguration.java new file mode 100644 index 0000000000..535504e70b --- /dev/null +++ b/server/pxf-api/src/main/java/org/greenplum/pxf/api/configuration/PxfJksTextEncryptorConfiguration.java @@ -0,0 +1,37 @@ +package org.greenplum.pxf.api.configuration; + +import io.arenadata.security.encryption.client.configuration.JksTextEncryptorConfiguration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty({"pxf.ssl.jks-store.path", "pxf.ssl.jks-store.password", "pxf.ssl.salt.key"}) +public class PxfJksTextEncryptorConfiguration extends JksTextEncryptorConfiguration { + private final String path; + private final String password; + private final String key; + + public PxfJksTextEncryptorConfiguration(@Value("${pxf.ssl.jks-store.path}") String path, + @Value("${pxf.ssl.jks-store.password}") String password, + @Value("${pxf.ssl.salt.key}") String key) { + this.path = path; + this.password = password; + this.key = key; + } + + @Override + protected String jksStorePath() { + return path; + } + + @Override + protected char[] jksStorePassword() { + return password.toCharArray(); + } + + @Override + protected String secretKeyAlias() { + return key; + } +} diff --git a/server/pxf-api/src/main/java/org/greenplum/pxf/api/utilities/SpringContext.java b/server/pxf-api/src/main/java/org/greenplum/pxf/api/utilities/SpringContext.java index bd28a417b1..3b45216f7e 100644 --- a/server/pxf-api/src/main/java/org/greenplum/pxf/api/utilities/SpringContext.java +++ b/server/pxf-api/src/main/java/org/greenplum/pxf/api/utilities/SpringContext.java @@ -26,6 +26,14 @@ public static T getBean(Class requiredType) { return context.getBean(requiredType); } + public static T getNullableBean(Class requiredType) { + try { + return context.getBean(requiredType); + } catch (Exception e) { + return null; + } + } + @Override public void setApplicationContext(ApplicationContext context) throws BeansException { diff --git a/server/pxf-jdbc/README.md b/server/pxf-jdbc/README.md index e5e96b976e..a2958f6fdf 100644 --- a/server/pxf-jdbc/README.md +++ b/server/pxf-jdbc/README.md @@ -102,7 +102,8 @@ User name (login) to use to connect to external database. #### JDBC password -Password to use to connect to external database. +Password to use to connect to external database. The password might be encrypted. +How to use encrypted password is described in section [JDBC password encryption](#jdbc-password-encryption). * **Option**: `PASS` * **Configuration parameter**: `jdbc.password` @@ -650,3 +651,109 @@ Follow these steps to enable connectivity to Hive: If you enable impersonation, do not explicitly specify `hive.server2.proxy.user` property in the URL. - if Hive is configured with `hive.server2.enable.doAs = FALSE`, Hive will run Hadoop operations with the identity provided by the PXF Kerberos principal (usually `gpadmin`) + + +## JDBC password encryption +It is possible to use an encrypted password instead of the password in a paint text in the `$PXF_BASE/servers//jdbc-site.xml` file. + +### Prerequisites +There is a special library that is used to encrypt and decrypt password. The executable jar-file of this library has to be copied to `$PXF_BASE/lib/` directory on each segment. +It is used to encrypt password. The original jar-file of the library is used to decrypt password. It is added as a dependency to the PXF project. + +### How to enable encryption +Before using an encrypted password you have to **create keystore and add encryption key** to the store.\ +The keystore is a file where the encryption key will be saved. And the encryption key will be used to encrypt and decrypt password.\ +The keystore and the encryption key have to be created on each segment server. + +The command to create the keystore:\ +```keytool -keystore -storepass -genkey -keypass -alias ```, where\ +`keystore_file` - the file path of the keystore;\ +`keystore_password` - password which will be used to access the keystore;\ +`key_password` - password for the specific `keystore_alias`. It might be the same as `keystore_password`;\ +`keystore_alias` - name of the keystore. + +You will be asked to enter some information about your organization, first and last name, etc. after running the command. + +Example of the command to create a keystore:\ +`keytool -keystore /var/lib/pxf/conf/pxfkeystore.jks -storepass 12345678 -genkey -keypass 12345678 -alias pxfkeystore` + +The next step is to add encryption key.\ +The command to add encryption key to the keystore:\ +`keytool -keystore -storepass -importpass -keypass -alias `, where\ +`keystore_file` - the file path of the keystore that was created in the previous step;\ +`keystore_password` - password to access the keystore;\ +`key_password` - password for the specific `encryption_key_alias`. It might be the same as `keystore_password`;\ +`encryption_key_alias` - name of the encryption key. This name will be used to get encryption key from the keystore. + +You will be asked to enter an encryption key you want to store after running the command. + +Example of the command to create a keystore:\ +`keytool -keystore /var/lib/pxf/conf/pxfkeystore.jks -storepass 12345678 -importpass -keypass 12345678 -alias PXF_PASS_KEY`\ +*Enter the password to be stored:* qwerty + +Next, additional properties have to be added into the `$PXF_BASE/conf/pxf-application.properties` file on each segment:\ +`pxf.ssl.jks-store.path` - a Java keystore (JKS) absolute file path. It is a `keystore_file` from the command to create the keystore;\ +`pxf.ssl.jks-store.password` - a Java keystore password. It is a `keystore_password` from the command to create the keystore;\ +`pxf.ssl.salt.key` - an alias which is used to get encryption key from the keystore. It is an `encryption_key_alias` from the command to add encryption key to the keystore. + +You have to restart PXF service after adding the properties. + +Example of the properties in the `pxf-application.properties` file: +``` +# Encryption +pxf.ssl.jks-store.path=/var/lib/pxf/conf/pxfkeystore.jks +pxf.ssl.jks-store.password=12345678 +pxf.ssl.salt.key=PXF_PASS_KEY +``` + +### How to use encryption +The first step is to encrypt password that is used to connect to the database.\ +There is a special command to do this action:\ +`pxf encrypt `, where\ +`` - password in a plain text that is used to connect to the database. This password will be encrypted;\ +`` - Optional. The algorithm to encrypt password. Default value: `aes256` + +The result of the command will be an encrypted password in a format `aes256:encrypted_password` + +Example of the command to encrypt password:\ +`pxf encrypt biuserpassword`\ +*Output:* aes256:7BhhI+10ut+xM70iRlyxVDD/tokap3pbK2bmkLgPOYLH7NcfEYJSAIYkApjKM3Zu + +Next, you have to copy the encrypted password including aes256 prefix and paste it into `$PXF_BASE/servers//jdbc-site.xml` file +instead of the password in a plain text. + +The example of the `jdbc-site.xml` with encrypted password: +```xml + + + + jdbc.driver + org.postgresql.Driver + Class name of the JDBC driver (e.g. org.postgresql.Driver) + + + jdbc.url + jdbc:postgresql://10.10.10.20/adb + The URL that the JDBC driver can use to connect to the database (e.g. jdbc:postgresql://localhost/postgres) + + + jdbc.user + bi_user + User name for connecting to the database (e.g. postgres) + + + jdbc.password + aes256:7BhhI+10ut+xM70iRlyxVDD/tokap3pbK2bmkLgPOYLH7NcfEYJSAIYkApjKM3Zu + Password for connecting to the database (e.g. postgres) + + +``` + +You don't need to make any additional changes when you crate an external table. The decryption engine will recognize whether the password is encrypted or not. +If the password is encrypted the decrypter will take care about the password. If the password is in plain text format it will be passed as is to the JDBC connection manager. + + + + + + diff --git a/server/pxf-jdbc/build.gradle b/server/pxf-jdbc/build.gradle index 0652609128..268987435e 100644 --- a/server/pxf-jdbc/build.gradle +++ b/server/pxf-jdbc/build.gradle @@ -39,6 +39,7 @@ dependencies { implementation("org.apache.hive.shims:hive-shims-0.23") { transitive = false } implementation("org.apache.hive.shims:hive-shims-common") { transitive = false } implementation("org.springframework.boot:spring-boot-starter-log4j2") + implementation("io.arenadata.security:encryption") /******************************* * Test Dependencies diff --git a/server/pxf-jdbc/src/main/java/org/greenplum/pxf/plugins/jdbc/JdbcAccessor.java b/server/pxf-jdbc/src/main/java/org/greenplum/pxf/plugins/jdbc/JdbcAccessor.java index 5c57054be2..4a7d562dd6 100644 --- a/server/pxf-jdbc/src/main/java/org/greenplum/pxf/plugins/jdbc/JdbcAccessor.java +++ b/server/pxf-jdbc/src/main/java/org/greenplum/pxf/plugins/jdbc/JdbcAccessor.java @@ -19,6 +19,7 @@ * under the License. */ +import io.arenadata.security.encryption.client.service.DecryptClient; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.greenplum.pxf.api.OneRow; @@ -78,8 +79,8 @@ public JdbcAccessor() { * @param connectionManager connection manager * @param secureLogin the instance of the secure login */ - JdbcAccessor(ConnectionManager connectionManager, SecureLogin secureLogin) { - super(connectionManager, secureLogin); + JdbcAccessor(ConnectionManager connectionManager, SecureLogin secureLogin, DecryptClient decryptClient) { + super(connectionManager, secureLogin, decryptClient); } /** diff --git a/server/pxf-jdbc/src/main/java/org/greenplum/pxf/plugins/jdbc/JdbcBasePlugin.java b/server/pxf-jdbc/src/main/java/org/greenplum/pxf/plugins/jdbc/JdbcBasePlugin.java index eda17b4950..65c0048e58 100644 --- a/server/pxf-jdbc/src/main/java/org/greenplum/pxf/plugins/jdbc/JdbcBasePlugin.java +++ b/server/pxf-jdbc/src/main/java/org/greenplum/pxf/plugins/jdbc/JdbcBasePlugin.java @@ -19,6 +19,7 @@ * under the License. */ +import io.arenadata.security.encryption.client.service.DecryptClient; import org.apache.commons.lang.StringUtils; import org.apache.hadoop.conf.Configuration; import org.greenplum.pxf.api.model.BasePlugin; @@ -39,10 +40,7 @@ import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Statement; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; +import java.util.*; import java.util.stream.Collectors; import static org.greenplum.pxf.api.security.SecureLogin.CONFIG_KEY_SERVICE_USER_IMPERSONATION; @@ -166,6 +164,7 @@ public static TransactionIsolation typeOf(String str) { private final ConnectionManager connectionManager; private final SecureLogin secureLogin; + private final DecryptClient decryptClient; static { // Deprecated as of Oct 22, 2019 in version 5.9.2+ @@ -176,10 +175,12 @@ public static TransactionIsolation typeOf(String str) { /** * Creates a new instance with default (singleton) instances of - * ConnectionManager and SecureLogin. + * ConnectionManager, SecureLogin and DecryptClient. */ JdbcBasePlugin() { - this(SpringContext.getBean(ConnectionManager.class), SpringContext.getBean(SecureLogin.class)); + this(SpringContext.getBean(ConnectionManager.class), SpringContext.getBean(SecureLogin.class), + SpringContext.getNullableBean(DecryptClient.class) + ); } /** @@ -187,9 +188,10 @@ public static TransactionIsolation typeOf(String str) { * * @param connectionManager connection manager instance */ - JdbcBasePlugin(ConnectionManager connectionManager, SecureLogin secureLogin) { + JdbcBasePlugin(ConnectionManager connectionManager, SecureLogin secureLogin, DecryptClient decryptClient) { this.connectionManager = connectionManager; this.secureLogin = secureLogin; + this.decryptClient = decryptClient; } @Override @@ -335,11 +337,15 @@ public void afterPropertiesSet() { ); } - // This must be the last parameter parsed, as we output connectionConfiguration earlier - // Optional parameter. By default, corresponding connectionConfiguration property is not set if (jdbcUser != null) { String jdbcPassword = configuration.get(JDBC_PASSWORD_PROPERTY_NAME); if (jdbcPassword != null) { + try { + jdbcPassword = decryptClient == null ? jdbcPassword : decryptClient.decrypt(jdbcPassword); + } catch (Exception e) { + throw new RuntimeException( + "Failed to decrypt jdbc password. " + e.getMessage(), e); + } LOG.debug("Connection password: {}", ConnectionManager.maskPassword(jdbcPassword)); connectionConfiguration.setProperty("password", jdbcPassword); } diff --git a/server/pxf-jdbc/src/test/java/org/greenplum/pxf/plugins/jdbc/JdbcAccessorTest.java b/server/pxf-jdbc/src/test/java/org/greenplum/pxf/plugins/jdbc/JdbcAccessorTest.java index 640e0c32c6..0a271d8b8c 100644 --- a/server/pxf-jdbc/src/test/java/org/greenplum/pxf/plugins/jdbc/JdbcAccessorTest.java +++ b/server/pxf-jdbc/src/test/java/org/greenplum/pxf/plugins/jdbc/JdbcAccessorTest.java @@ -1,5 +1,6 @@ package org.greenplum.pxf.plugins.jdbc; +import io.arenadata.security.encryption.client.service.DecryptClient; import org.apache.hadoop.conf.Configuration; import org.greenplum.pxf.api.model.RequestContext; import org.greenplum.pxf.api.security.SecureLogin; @@ -49,11 +50,13 @@ public class JdbcAccessorTest { private PreparedStatement mockPreparedStatement; @Mock private ResultSet mockResultSet; + @Mock + private DecryptClient mockDecryptClient; @BeforeEach public void setup() { - accessor = new JdbcAccessor(mockConnectionManager, mockSecureLogin); + accessor = new JdbcAccessor(mockConnectionManager, mockSecureLogin, mockDecryptClient); configuration = new Configuration(); context = new RequestContext(); context.setConfig("default"); diff --git a/server/pxf-jdbc/src/test/java/org/greenplum/pxf/plugins/jdbc/JdbcBasePluginTest.java b/server/pxf-jdbc/src/test/java/org/greenplum/pxf/plugins/jdbc/JdbcBasePluginTest.java index a1ed1c0347..dd405b12bb 100644 --- a/server/pxf-jdbc/src/test/java/org/greenplum/pxf/plugins/jdbc/JdbcBasePluginTest.java +++ b/server/pxf-jdbc/src/test/java/org/greenplum/pxf/plugins/jdbc/JdbcBasePluginTest.java @@ -19,6 +19,7 @@ * under the License. */ +import io.arenadata.security.encryption.client.service.DecryptClient; import org.apache.hadoop.conf.Configuration; import org.greenplum.pxf.api.model.RequestContext; import org.greenplum.pxf.api.security.SecureLogin; @@ -64,6 +65,8 @@ public class JdbcBasePluginTest { private PreparedStatement mockStatement; @Mock private SecureLogin mockSecureLogin; + @Mock + private DecryptClient mockDecryptClient; private final SQLException exception = new SQLException("some error"); private Configuration configuration; @@ -238,7 +241,7 @@ public void testTransactionIsolationNotSetByUser() throws SQLException { when(mockConnectionManager.getConnection(any(), any(), any(), anyBoolean(), any(), any())).thenReturn(mockConnection); when(mockConnection.getMetaData()).thenReturn(mockMetaData); - JdbcBasePlugin plugin = new JdbcBasePlugin(mockConnectionManager, mockSecureLogin); + JdbcBasePlugin plugin = new JdbcBasePlugin(mockConnectionManager, mockSecureLogin, mockDecryptClient); plugin.setRequestContext(context); Connection conn = plugin.getConnection(); @@ -297,7 +300,7 @@ public void testTransactionIsolationSetByUserFailedToGetMetadata() throws SQLExc when(mockConnectionManager.getConnection(anyString(), anyString(), any(), anyBoolean(), any(), anyString())).thenReturn(mockConnection); doThrow(new SQLException("")).when(mockConnection).getMetaData(); - JdbcBasePlugin plugin = new JdbcBasePlugin(mockConnectionManager, mockSecureLogin); + JdbcBasePlugin plugin = new JdbcBasePlugin(mockConnectionManager, mockSecureLogin, mockDecryptClient); plugin.setRequestContext(context); assertThrows(SQLException.class, plugin::getConnection); } @@ -323,7 +326,7 @@ public void testGetPreparedStatementDoesNotSetQueryTimeoutIfNotSpecified() throw when(mockConnection.prepareStatement(anyString())).thenReturn(mockStatement); - JdbcBasePlugin plugin = new JdbcBasePlugin(mockConnectionManager, mockSecureLogin); + JdbcBasePlugin plugin = new JdbcBasePlugin(mockConnectionManager, mockSecureLogin, mockDecryptClient); plugin.setRequestContext(context); plugin.getPreparedStatement(mockConnection, "foo"); @@ -485,7 +488,7 @@ public void testGetConnectionConnPropsPoolDisabledPoolProps() throws SQLExceptio } private JdbcBasePlugin getPlugin(ConnectionManager mockConnectionManager, SecureLogin mockSecureLogin, RequestContext context) { - JdbcBasePlugin plugin = new JdbcBasePlugin(mockConnectionManager, mockSecureLogin); + JdbcBasePlugin plugin = new JdbcBasePlugin(mockConnectionManager, mockSecureLogin, mockDecryptClient); plugin.setRequestContext(context); plugin.afterPropertiesSet(); return plugin; diff --git a/server/pxf-jdbc/src/test/java/org/greenplum/pxf/plugins/jdbc/JdbcBasePluginTestInitialize.java b/server/pxf-jdbc/src/test/java/org/greenplum/pxf/plugins/jdbc/JdbcBasePluginTestInitialize.java index 6b0cdac06d..185cc8bbe8 100644 --- a/server/pxf-jdbc/src/test/java/org/greenplum/pxf/plugins/jdbc/JdbcBasePluginTestInitialize.java +++ b/server/pxf-jdbc/src/test/java/org/greenplum/pxf/plugins/jdbc/JdbcBasePluginTestInitialize.java @@ -20,6 +20,8 @@ */ import com.google.common.base.Ticker; +import io.arenadata.security.encryption.client.provider.TextEncryptorProvider; +import io.arenadata.security.encryption.client.service.impl.DecryptClientImpl; import org.apache.commons.collections.CollectionUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.security.PxfUserGroupInformation; @@ -89,7 +91,8 @@ public void setup() { ); PxfUserGroupInformation mockPxfUserGroupInformation = mock(PxfUserGroupInformation.class); - plugin = new JdbcBasePlugin(connectionManager, new SecureLogin(mockPxfUserGroupInformation)); + TextEncryptorProvider mockTextEncryptorProvider = mock(TextEncryptorProvider.class); + plugin = new JdbcBasePlugin(connectionManager, new SecureLogin(mockPxfUserGroupInformation), new DecryptClientImpl(mockTextEncryptorProvider)); } /** diff --git a/server/pxf-service/src/scripts/pxf b/server/pxf-service/src/scripts/pxf index e011eeec67..775166f173 100755 --- a/server/pxf-service/src/scripts/pxf +++ b/server/pxf-service/src/scripts/pxf @@ -227,6 +227,7 @@ function doHelp() It creates the servers, logs, lib, keytabs, and run directories inside \$PXF_BASE and copies configuration files. migrate migrates configurations from older installations of PXF + encrypt encrypt password with specified encryptor type. Default encryptor type is aes256 cluster perform on all the segment hosts in the cluster; try ${bold}pxf cluster help$normal sync synchronize \$PXF_BASE/{conf,lib,servers} directories onto . Use --delete to delete extraneous remote files @@ -492,6 +493,35 @@ function doSync() rsync -az${DELETE:+ --delete} -e "ssh -o StrictHostKeyChecking=no" "$PXF_BASE"/{conf,lib,servers} "${target_host}:$PXF_BASE" } +function doEncrypt() +{ + local pwd=$1 + local encryptorType=$2 + if [[ -z $pwd ]]; then + fail 'Please provide password you want to encrypt' + fi + if [[ -z $encryptorType ]]; then + encryptorType=aes256 + fi + conf_file="$PXF_BASE/conf/pxf-application.properties" + if [[ -z $conf_file ]]; then + fail "File 'pxf-application.properties' was not found in PXF_BASE/conf/ directory" + fi + encr_jar_file=$(find "$PXF_BASE/lib" -maxdepth 1 -type f -name 'encryption-*.jar') + if [[ -z $encr_jar_file ]]; then + fail "Encryption library was not found in $PXF_BASE/lib/ directory" + fi + jksPath=$(getProperty 'pxf.ssl.jks-store.path' "$conf_file") + jksPassword=$(getProperty 'pxf.ssl.jks-store.password' "$conf_file") + jksEncryptKeyAlias=$(getProperty 'pxf.ssl.salt.key' "$conf_file") + checkJavaHome + java -jar $encr_jar_file -command encrypt -jksPath $jksPath -jksPassword $jksPassword -jksEncryptKeyAlias $jksEncryptKeyAlias -message $pwd -encryptorType $encryptorType +} + +function getProperty { + grep "^${1}" ${2} | cut -d'=' -f2 +} + function doCluster() { local cmd=$2 @@ -545,6 +575,9 @@ case $pxf_script_command in doSync "$2" fi ;; + 'encrypt') + doEncrypt "$2" "$3" + ;; 'help' | '-h' | '--help') doHelp ;;