IDEMPIERE-4602 Encrypt passwords on properties files (FHCA-1982) (#498)

* backward compatible with the previous idempiere.properties and idempiereEnv.properties version
* when the setup/console-setup is executed again the secret keys are stored obfuscated in a different file .idpass
  * Secret keys are ADEMPIERE_DB_PASSWORD, ADEMPIERE_DB_SYSTEM, ADEMPIERE_MAIL_PASSWORD
  * to add more is just adding keys to array ConfigurationData.secretVars
* the previous (unobfuscated) approach is still preserved passing -DIDEMPIERE_SECURE_PROPERTIES=false to the JVM in setup and server
* the approach just run on Linux - as is implemented using shell script, windows is out of the initial scope, but could be possible to implement .bat files to do similar
* the default approach is to use getVar.sh and setVar.sh that writes in .idpass obfuscated
  * is possible to extend and use custom secret managers implementing customSetVar.sh and customGetVar.sh
  * samples for amazon AWS secretsmanager are included
* avoid the email sent on setup sending the secret keys
* enclose all variables in myEnvironment.sh within quotes (this avoids problems with variables containing spaces)
* add coreutils as required for debian installer (as we use base64 now to obfuscate passwords)
* swing client is not affected as it saves the idempiere.properties encrypted in user home folder
This commit is contained in:
Carlos Ruiz 2021-01-03 15:19:49 +01:00 committed by GitHub
parent 11dc7a391f
commit 47aee6fb02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 289 additions and 43 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@
/Oracle/
/log/
/*.log
/.idpass
/idempiere.properties
/idempiereEnv.properties
/hazelcast.xml

View File

@ -48,7 +48,15 @@ ADEMPIERE_DB_NAME="$(expr "$CONN" : ".*DBname.=\(.*\),BQ.=")"
ADEMPIERE_DB_SERVER="$(expr "$CONN" : ".*DBhost.=\(.*\),DBport.=")"
ADEMPIERE_DB_PORT="$(expr "$CONN" : ".*DBport.=\(.*\),DBname.=")"
ADEMPIERE_DB_USER="$(expr "$CONN" : ".*UID.=\(.*\),PWD.=")"
if [ "x$ADEMPIERE_DB_USER" = "x" ]
then
ADEMPIERE_DB_USER="$(expr "$CONN" : ".*UID.=\(.*\)\]")"
fi
ADEMPIERE_DB_PASSWORD="$(expr "$CONN" : ".*PWD.=\(.*\)]")"
if [ "x$ADEMPIERE_DB_PASSWORD" = "x" ]
then
ADEMPIERE_DB_PASSWORD="$( $IDEMPIERE_HOME/org.adempiere.server-feature/utils.unix/getVar.sh ADEMPIERE_DB_PASSWORD )"
fi
ADEMPIERE_DB_PATH="$(expr "$CONN" : ".*type.=\(.*\),DBhost.=")"
ADEMPIERE_DB_PATH="${ADEMPIERE_DB_PATH,,}"
export IDEMPIERE_HOME

View File

@ -26,6 +26,7 @@ import javax.naming.InitialContext;
import javax.sql.DataSource;
import javax.swing.JOptionPane;
import org.compiere.model.MSystem;
import org.compiere.util.CLogger;
import org.compiere.util.Ini;
@ -41,7 +42,7 @@ public class CConnection implements Serializable, Cloneable
/**
*
*/
private static final long serialVersionUID = -858558852550858165L;
private static final long serialVersionUID = -1823868178123935209L;
/** Connection */
private volatile static CConnection s_cc = null;
@ -59,6 +60,13 @@ public class CConnection implements Serializable, Cloneable
String attributes = Ini.getProperty (Ini.P_CONNECTION);
s_cc = new CConnection (null);
s_cc.setAttributes (attributes);
if (! attributes.contains("PWD=") && MSystem.isSecureProps())
{
// get the password from secret properties
String dbPwd = Ini.getVar("ADEMPIERE_DB_PASSWORD");
if (dbPwd != null)
s_cc.setDbPwd(dbPwd);
}
if (log.isLoggable(Level.FINE)) log.fine(s_cc.toString());
}
@ -865,11 +873,23 @@ public class CConnection implements Serializable, Cloneable
/**
* String representation.
* Used also for Instanciation
* Used also for Instantiation
* @return string representation
* @see #setAttributes(String) setAttributes
*/
public String toStringLong ()
{
return toStringLong(true);
}
/**
* String representation.
* Used also for Instantiation
* @param includePass flag to include the password in the String
* @return string representation
* @see #setAttributes(String) setAttributes
*/
public String toStringLong (boolean includePass)
{
StringBuilder sb = new StringBuilder ("CConnection[");
sb.append ("name=").append (escape(m_name))
@ -885,9 +905,11 @@ public class CConnection implements Serializable, Cloneable
.append (",FWhost=").append (escape(m_fw_host))
.append (",FWport=").append (m_fw_port)
.append (",UID=").append (escape(m_db_uid))
.append (",PWD=").append (escape(m_db_pwd))
.append("]");
; // the format is read by setAttributes
if (includePass) {
sb.append (",PWD=").append (escape(m_db_pwd));
}
sb.append("]");
return sb.toString ();
} // toStringLong

View File

@ -56,7 +56,7 @@ public class MSystem extends X_AD_System
/**
*
*/
private static final long serialVersionUID = 8639311032004561198L;
private static final long serialVersionUID = 3090872841676580202L;
/**
* Load System Record
@ -521,5 +521,21 @@ public class MSystem extends X_AD_System
String ca = MSysConfig.getValue(MSysConfig.SWING_LOGIN_ALLOW_REMEMBER_ME, SYSTEM_ALLOW_REMEMBER_PASSWORD);
return (ca.equalsIgnoreCase(SYSTEM_ALLOW_REMEMBER_PASSWORD) && !MSysConfig.getBooleanValue(MSysConfig.USER_PASSWORD_HASH, false));
}
/**
* Verify if the system manages properties in a more secure way
* for Windows and swing client the properties are managed as always
* for other systems (like Linux) the default is to manage it with more security
* this can be overridden passing the parameter -DIDEMPIERE_SECURE_PROPERTIES=false to the JVM
* @return true if properties needs to be managed more secure
*/
public static boolean isSecureProps() {
if (Env.isWindows() || Ini.isClient())
return false;
String secureProps = System.getProperty("IDEMPIERE_SECURE_PROPERTIES");
if (secureProps != null && secureProps.equals("false"))
return false;
return true;
}
} // MSystem

View File

@ -218,7 +218,11 @@ public final class DB
String mailUser = env.getProperty("ADEMPIERE_MAIL_USER");
if (mailUser == null || mailUser.length() == 0)
return;
String mailPassword = env.getProperty("ADEMPIERE_MAIL_PASSWORD");
String mailPassword;
if (!env.containsKey("ADEMPIERE_MAIL_PASSWORD") && MSystem.isSecureProps())
mailPassword = Ini.getVar("ADEMPIERE_MAIL_PASSWORD");
else
mailPassword = env.getProperty("ADEMPIERE_MAIL_PASSWORD");
// if (mailPassword == null || mailPassword.length() == 0)
// return;
//
@ -239,13 +243,11 @@ public final class DB
no = DB.executeUpdate(sql.toString(), null);
if (log.isLoggable(Level.FINE)) log.fine("User #"+no);
//
try
try (FileOutputStream out = new FileOutputStream(envFile))
{
env.setProperty("ADEMPIERE_MAIL_UPDATED", "Y");
FileOutputStream out = new FileOutputStream(envFile);
env.store(out, "");
out.flush();
out.close();
}
catch (Exception e)
{

View File

@ -168,6 +168,8 @@ public class DBReadReplica {
m_pass = value;
}
}
if (m_pass == null && MSystem.isSecureProps())
m_pass = Ini.getVar("ADEMPIERE_DB_PASSWORD");
} catch (Exception e) {
log.log(Level.SEVERE, attributes + " - " + e.toString (), e);
}

View File

@ -18,18 +18,22 @@ package org.compiere.util;
import java.awt.Dimension;
import java.awt.Point;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import org.adempiere.exceptions.AdempiereException;
import org.compiere.model.ModelValidationEngine;
/**
@ -62,10 +66,10 @@ public final class Ini implements Serializable
private static final String DEFAULT_UID = "GardenAdmin";
/** Apps Password */
public static final String P_PWD = "ApplicationPassword";
private static final String DEFAULT_PWD = "GardenAdmin";
private static final String DEFAULT_PWD = "";
/** Store Password */
public static final String P_STORE_PWD = "StorePassword";
private static final boolean DEFAULT_STORE_PWD = true;
private static final boolean DEFAULT_STORE_PWD = false;
/** Trace Level */
public static final String P_TRACELEVEL = "TraceLevel";
private static final String DEFAULT_TRACELEVEL = "WARNING";
@ -794,4 +798,76 @@ public final class Ini implements Serializable
{
return s_propertyFileName;
}
public static String getVar(String secretVar) {
String cmd = getUtilsCmd("getVar");
String[] command = new String[] {
cmd,
secretVar
};
String retValue = runCommand(command);
return retValue;
}
public static void setVar(String secretVar, String secretValue) {
String cmd = getUtilsCmd("setVar");
String[] command = new String[] {
cmd,
secretVar,
secretValue
};
@SuppressWarnings("unused") // used for debugging purposes
String retValue = runCommand(command);
}
private static String getUtilsCmd(String script) {
File utilsFolder = new File(getAdempiereHome() + File.separator + "utils");
if (! utilsFolder.exists()) {
// /utils does not exist, probably running on eclipse
if (Env.isWindows()) {
utilsFolder = new File(getAdempiereHome() + File.separator + "org.adempiere.server-feature" + File.separator + "utils.windows");
} else {
utilsFolder = new File(getAdempiereHome() + File.separator + "org.adempiere.server-feature" + File.separator + "utils.unix");
}
if (! utilsFolder.exists()) {
throw new AdempiereException("Folder utils does not exist");
}
}
File cmd = new File(utilsFolder, script + (Env.isWindows() ? ".bat" : ".sh"));
if (! cmd.exists() || ! cmd.canExecute()) {
throw new AdempiereException("File does not exist or canno execute " + cmd.getAbsolutePath());
}
return cmd.getAbsolutePath();
}
public static String runCommand(String[] command) {
StringBuilder msg = new StringBuilder();
try {
String s;
Process p = Runtime.getRuntime().exec(command);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
BufferedReader stdError = new BufferedReader(new InputStreamReader(p.getErrorStream()));
// read the output from the command
while ((s = stdInput.readLine()) != null) {
msg.append(s);
}
// read any errors from the attempted command
while ((s = stdError.readLine()) != null) {
msg.append(s);
}
try {
p.waitFor(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new AdempiereException("Timeout waiting for " + command[0], e);
}
if (p.exitValue() != 0) {
throw new Exception(msg.toString());
}
}
catch (Exception e) {
throw new AdempiereException("Could not execute " + command[0], e);
}
return msg.toString();
}
} // Ini

View File

@ -29,6 +29,7 @@ import java.net.URLConnection;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
@ -50,6 +51,7 @@ import org.adempiere.install.IDatabaseConfig;
import org.compiere.Adempiere;
import org.compiere.db.CConnection;
import org.compiere.db.Database;
import org.compiere.model.MSystem;
import org.compiere.util.CLogMgt;
import org.compiere.util.CLogger;
import org.compiere.util.DB;
@ -195,6 +197,11 @@ public class ConfigurationData
/** */
public static final String ADEMPIERE_WEBSTORES = "ADEMPIERE_WEBSTORES";
public static final List<String> secretVars = Arrays.asList(new String[] {
ADEMPIERE_DB_PASSWORD,
ADEMPIERE_DB_SYSTEM,
ADEMPIERE_MAIL_PASSWORD
});
public void updateProperty(String property, String value) {
if (value == null) value = "";
@ -240,6 +247,15 @@ public class ConfigurationData
if (p_properties.size() > 5)
envLoaded = true;
if (MSystem.isSecureProps()) {
// add secret variables reading with getVar
for (String secretVar : secretVars) {
if (! p_properties.containsKey(secretVar)) {
String val = Ini.getVar(secretVar);
p_properties.put(secretVar, val);
}
}
}
// deobfuscate keystore pass
String obfKeystorePass = p_properties.getProperty(ADEMPIERE_KEYSTOREPASS);
if (obfKeystorePass.startsWith(Password.__OBFUSCATE)) {
@ -634,11 +650,23 @@ public class ConfigurationData
try
{
String admail = adminEMail.toString();
StringBuilder msg = new StringBuilder("Test: \n");
getProperties().forEach((k, v) -> {
String key = k.toString();
String value = v.toString();
msg.append(key).append("=");
if (secretVars.contains(key)) {
msg.append("********");
} else {
msg.append(value);
}
msg.append("\n");
});
EMail email = new EMail (new Properties(),
mailServer.getHostName (),
admail, admail,
"Adempiere Server Setup Test",
"Test: " + getProperties());
"iDempiere Server Setup Test",
msg.toString());
email.createAuthenticator (mailUser, mailPassword);
if (EMail.SENT_OK.equals (email.send ()))
{
@ -841,12 +869,27 @@ public class ConfigurationData
try
{
fos = new FileOutputStream(new File(fileName));
Properties secretProperties = new Properties();
if (MSystem.isSecureProps()) {
// separate secret variables from properties and save them with setVar
for (String secretVar : secretVars) {
String secretValue = p_properties.getProperty(secretVar);
if (secretValue != null) {
secretProperties.put(secretVar, secretValue);
Ini.setVar(secretVar, secretValue);
}
p_properties.remove(secretVar);
}
}
// obfuscate keystore pass
String keystorePass = p_properties.getProperty(ADEMPIERE_KEYSTOREPASS);
String obfKeystorePass = Password.obfuscate(keystorePass);
p_properties.put(ADEMPIERE_KEYSTOREPASS, obfKeystorePass);
p_properties.store(fos, IDEMPIERE_ENV_FILE);
p_properties.put(ADEMPIERE_KEYSTOREPASS, keystorePass);
// put back secrets in properties
if (MSystem.isSecureProps())
p_properties.putAll(secretProperties);
fos.flush();
}
catch (Exception e)
@ -922,7 +965,12 @@ public class ConfigurationData
log.log(Level.SEVERE, "connection", e);
return false;
}
Ini.setProperty(Ini.P_CONNECTION, cc.toStringLong());
if (MSystem.isSecureProps()) {
// do not save PWD to the attributes - must be obtained with getVar
Ini.setProperty(Ini.P_CONNECTION, cc.toStringLong(false));
} else {
Ini.setProperty(Ini.P_CONNECTION, cc.toStringLong(true));
}
Ini.saveProperties(false);
return true;
} // saveIni

View File

@ -101,7 +101,12 @@
<chmod dir="." perm="ugo+x" includes="**/*.sh" />
<chmod file="idempiereEnv.properties" perm="0600"/>
<chmod file="idempiere.properties" perm="0600"/>
<chmod file=".idpass" perm="0600"/>
<chmod file="utils/myEnvironment.sh" perm="0700"/>
<chmod file="utils/getVar.sh" perm="0700"/>
<chmod file="utils/setVar.sh" perm="0700"/>
<chmod file="utils/customGetVar.sh" perm="0700"/>
<chmod file="utils/customSetVar.sh" perm="0700"/>
<delete>
<fileset dir="." includes="*.bat" />

View File

@ -9,9 +9,5 @@ echo ... Setup Jetty
# Setup Jetty
./idempiere --launcher.ini setup.ini -application org.eclipse.ant.core.antRunner -buildfile build.xml
echo ... Make .sh executable
chmod -R a+x -- *.sh
find . -name '*.sh' -exec chmod a+x '{}' \;
echo ...
echo For problems, check log file in base directory

View File

@ -0,0 +1,7 @@
#!/bin/bash
VARIABLE="${1}"
# export AWS_ACCESS_KEY_ID=?
# export AWS_SECRET_ACCESS_KEY=?
# export AWS_DEFAULT_REGION=?
echo $( aws secretsmanager get-secret-value --secret-id "${VARIABLE}" --query SecretString --output text )
exit 0

View File

@ -0,0 +1,11 @@
#!/bin/bash
VARIABLE="${1}"
VALUE="${2}"
# export AWS_ACCESS_KEY_ID=?
# export AWS_SECRET_ACCESS_KEY=?
# export AWS_DEFAULT_REGION=?
aws secretsmanager create-secret --name "${VARIABLE}" --secret-string "${VALUE}"
if [ $? != 0 ]
then
aws secretsmanager update-secret --secret-id "${VARIABLE}" --secret-string "${VALUE}"
fi

View File

@ -0,0 +1,22 @@
#!/bin/bash
VARIABLE="${1}"
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
if [ -x "$DIR/customGetVar.sh" ]
then
"$DIR/customGetVar.sh" "$VARIABLE"
else
if [[ "$DIR" == *utils ]]
then
VARFILE="$DIR/../.idpass"
else
VARFILE="$DIR/../../.idpass"
fi
if [ ! -f "$VARFILE" ]
then
echo "Variables file does not exist"
exit 1
fi
source "$VARFILE"
echo "${!VARIABLE}" | base64 -d
fi
exit 0

View File

@ -1,7 +1,7 @@
#!/bin/sh
#
# myEnvironment defines the variables used for idempiere
# Do not edit directly - use RUN_setup
# Do not edit directly - use setup.sh
#
# $Id: myEnvironmentTemplate.sh,v 1.12 2005/02/21 03:17:21 jjanke Exp $
@ -12,57 +12,54 @@ echo Setting myEnvironment ....
# Server install needs to change
# ADEMPIERE_DB_NAME (for Oracle)
# passwords
#
# For a HTML browser, idempiere will call "netscape <targetURL>"
# If not in the path, provide a link called netscape to your browser
# Homes ...
IDEMPIERE_HOME=@IDEMPIERE_HOME@
IDEMPIERE_HOME="@IDEMPIERE_HOME@"
export IDEMPIERE_HOME
JAVA_HOME=@JAVA_HOME@
JAVA_HOME="@JAVA_HOME@"
export JAVA_HOME
# Database ...
ADEMPIERE_DB_SERVER=@ADEMPIERE_DB_SERVER@
ADEMPIERE_DB_SERVER="@ADEMPIERE_DB_SERVER@"
export ADEMPIERE_DB_SERVER
ADEMPIERE_DB_USER=@ADEMPIERE_DB_USER@
ADEMPIERE_DB_USER="@ADEMPIERE_DB_USER@"
export ADEMPIERE_DB_USER
ADEMPIERE_DB_PASSWORD=@ADEMPIERE_DB_PASSWORD@
ADEMPIERE_DB_PASSWORD="$( "$IDEMPIERE_HOME/utils/getVar.sh" ADEMPIERE_DB_PASSWORD )"
export ADEMPIERE_DB_PASSWORD
ADEMPIERE_DB_URL=@ADEMPIERE_DB_URL@
ADEMPIERE_DB_URL="@ADEMPIERE_DB_URL@"
export ADEMPIERE_DB_URL
ADEMPIERE_DB_PORT=@ADEMPIERE_DB_PORT@
ADEMPIERE_DB_PORT="@ADEMPIERE_DB_PORT@"
export ADEMPIERE_DB_PORT
# Oracle Specifics ...
ADEMPIERE_DB_PATH=@ADEMPIERE_DB_PATH@
ADEMPIERE_DB_PATH="@ADEMPIERE_DB_PATH@"
export ADEMPIERE_DB_PATH
ADEMPIERE_DB_NAME=@ADEMPIERE_DB_NAME@
ADEMPIERE_DB_NAME="@ADEMPIERE_DB_NAME@"
export ADEMPIERE_DB_NAME
ADEMPIERE_DB_SYSTEM=@ADEMPIERE_DB_SYSTEM@
ADEMPIERE_DB_SYSTEM="$( "$IDEMPIERE_HOME/utils/getVar.sh" ADEMPIERE_DB_SYSTEM )"
export ADEMPIERE_DB_SYSTEM
# Homes(2)
ADEMPIERE_DB_HOME=$IDEMPIERE_HOME/utils/$ADEMPIERE_DB_PATH
ADEMPIERE_DB_HOME="$IDEMPIERE_HOME/utils/$ADEMPIERE_DB_PATH"
export ADEMPIERE_DB_HOME
# Apps Server
ADEMPIERE_APPS_SERVER=@ADEMPIERE_APPS_SERVER@
ADEMPIERE_APPS_SERVER="@ADEMPIERE_APPS_SERVER@"
export ADEMPIERE_APPS_SERVER
ADEMPIERE_WEB_PORT=@ADEMPIERE_WEB_PORT@
ADEMPIERE_WEB_PORT="@ADEMPIERE_WEB_PORT@"
export ADEMPIERE_WEB_PORT
ADEMPIERE_JNP_PORT=@ADEMPIERE_JNP_PORT@
ADEMPIERE_JNP_PORT="@ADEMPIERE_JNP_PORT@"
export ADEMPIERE_JNP_PORT
# SSL Settings - see jboss/server/adempiere/deploy/jbossweb.sar/META-INF/jboss-service.xml
ADEMPIERE_SSL_PORT=@ADEMPIERE_SSL_PORT@
# SSL Settings - see jettyhome/etc/jetty.xml and jettyhome/etc/jetty-ssl.xml
ADEMPIERE_SSL_PORT="@ADEMPIERE_SSL_PORT@"
export ADEMPIERE_SSL_PORT
ADEMPIERE_KEYSTORE=@ADEMPIERE_KEYSTORE@
ADEMPIERE_KEYSTORE="@ADEMPIERE_KEYSTORE@"
export ADEMPIERE_KEYSTORE
ADEMPIERE_KEYSTOREPASS=@ADEMPIERE_KEYSTOREPASS@
ADEMPIERE_KEYSTOREPASS="@ADEMPIERE_KEYSTOREPASS@"
export ADEMPIERE_KEYSTOREPASS
# Java
ADEMPIERE_JAVA=$JAVA_HOME/bin/java
ADEMPIERE_JAVA="$JAVA_HOME/bin/java"
export ADEMPIERE_JAVA
IDEMPIERE_JAVA_OPTIONS="@IDEMPIERE_JAVA_OPTIONS@ -DIDEMPIERE_HOME=$IDEMPIERE_HOME"
export IDEMPIERE_JAVA_OPTIONS

View File

@ -0,0 +1,33 @@
#!/bin/bash
VARIABLE="${1}"
VALUE="${2}"
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
if [ -x "$DIR/customSetVar.sh" ]
then
"$DIR/customSetVar.sh" "$VARIABLE" "$VALUE"
else
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
if [[ "$DIR" == *utils ]]
then
VARFILE="$DIR/../.idpass"
else
VARFILE="$DIR/../../.idpass"
fi
if [ ! -f "$VARFILE" ]
then
: > "$VARFILE"
fi
chmod 600 "$VARFILE"
if grep -q "^$VARIABLE=" "$VARFILE"
then
sed -i -e "/^$VARIABLE=/d" "$VARFILE"
fi
if [ "x$VALUE" == "x" ]
then
echo "$VARIABLE=" >> "$VARFILE"
else
VALB=$( echo "$VALUE" | base64 )
echo "$VARIABLE=\"$VALB\"" >> "$VARFILE"
fi
fi
exit 0

View File

@ -3,7 +3,7 @@ Version: 8.2
Section: web
Priority: extra
Architecture: all
Pre-Depends: openjdk-11-jdk-headless|openjdk-12-jdk-headless|openjdk-13-jdk-headless|openjdk-14-jdk-headless, postgresql-13|postgresql-12|postgresql-11|postgresql-10|postgresql-contrib-9.6, adduser, net-tools
Pre-Depends: openjdk-11-jdk-headless|openjdk-12-jdk-headless|openjdk-13-jdk-headless|openjdk-14-jdk-headless, postgresql-13|postgresql-12|postgresql-11|postgresql-10|postgresql-contrib-9.6, adduser, net-tools, coreutils
Suggests: firefox
Installed-Size: 968668
Maintainer: Carlos Ruiz <carg67@gmail.com>