如何避免为 tomcat 的 server.xml 数据源的资源定义明文存储密码?

Posted

技术标签:

【中文标题】如何避免为 tomcat 的 server.xml 数据源的资源定义明文存储密码?【英文标题】:How to avoid storing passwords in the clear for tomcat's server.xml Resource definition of a DataSource? 【发布时间】:2010-09-12 20:25:24 【问题描述】:

tomcat 的server.xml 中的资源定义看起来像这样...

<Resource
    name="jdbc/tox"
    scope="Shareable"
    type="javax.sql.DataSource"
    url="jdbc:oracle:thin:@yourDBserver.yourCompany.com:1521:yourDBsid"
    driverClassName="oracle.jdbc.pool.OracleDataSource"
    username="tox"
    password="toxbaby"
    maxIdle="3"
    maxActive="10"
    removeAbandoned="true"
    removeAbandonedTimeout="60"
    testOnBorrow="true"
    validationQuery="select * from dual"
    logAbandoned="true"
    debug="99"/>

密码是明文的。如何避免这种情况?

【问题讨论】:

【参考方案1】:

如前所述,加密密码只是将问题转移到其他地方。

无论如何,这很简单。 只需为您的密钥等编写一个包含静态字段的类,以及加密、解密密码的静态方法。 使用此类在 Tomcat 的配置文件(server.xmlyourapp.xml...)中加密您的密码。

要在 Tomcat 中“即时”解密密码,扩展 DBCP 的 BasicDataSourceFactory 并在您的资源中使用这个工厂。

它看起来像:

    <Resource
        name="jdbc/myDataSource"
        auth="Container"
        type="javax.sql.DataSource"
        username="user"
        password="encryptedpassword"
        driverClassName="driverClass"
        factory="mypackage.MyCustomBasicDataSourceFactory"
        url="jdbc:blabla://..."/>

对于自定义工厂:

package mypackage;

....

public class MyCustomBasicDataSourceFactory extends org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory 

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception 
    Object o = super.getObjectInstance(obj, name, nameCtx, environment);
    if (o != null) 
        BasicDataSource ds = (BasicDataSource) o;
        if (ds.getPassword() != null && ds.getPassword().length() > 0) 
            String pwd = MyPasswordUtilClass.unscramblePassword(ds.getPassword());
            ds.setPassword(pwd);
        
        return ds;
     else 
        return null;
    

希望这会有所帮助。

【讨论】:

对我来说,这并没有增加任何安全性,因为攻击者总是可以反编译您的 jar 以获取您在类文件中硬编码的密码... IMO 它仍然比在每个愚蠢的脚本小子都知道要查找它的地方清楚密码要好。它提高了一些标准,不是吗? @dmansfield - 我错过了什么吗?在上述方法中,我没有看到任何类文件中的密码会被硬编码在哪里? @AjoyBhatia - MyPasswordUtilClass.unscramblePassword(String s) 如何进行解密?它必须有一个可用于解密的秘密,或者更糟糕的是,使用非密钥算法(例如 rot13)来解密。所以这真的只是把问题推到了 MyPasswordUtilClass - 秘密来自哪里? 您可以从外部文件中读取密码,该文件只能由运行 tomcat 的用户读取(同时仍将其与其余配置分开)【参考方案2】:

Tomcat 有 a Password FAQ 专门解决您的问题。简而言之:保持密码清晰并正确锁定您的服务器。

该页面还提供了一些关于如何使用隐匿安全性来通过审核员检查表的建议。

【讨论】:

不过,您可能希望将包含密码的文件与配置的其余部分分开保存。在某些情况下,人们希望将配置文件放入某个版本控制系统中,所有相关的开发人员都可以阅读,而不仅仅是系统管理员。 这几乎没有解决任何问题。我无法将配置文件提交到带有密码的版本控制,但 context.xml 在所有部署中都必须相同,因此他们的“审计员”是个稻草人。 仅供参考:我冒昧地将常见问题解答链接更新到新位置【参考方案3】:

正如@Ryan 提到的,请在实施此解决方案之前阅读 Tomcat 的Tomcat Password FAQ。您只是增加了晦涩性而不是安全性。

@Jerome Delattre 的答案适用于简单的 JDBC 数据源,但不适用于作为数据源构造的一部分连接的更复杂的数据源(例如 oracle.jdbc.xa.client.OracleXADataSource)。

这是在调用现有工厂之前修改密码的替代方法。下面是一个用于基本数据源的工厂和一个用于 Atomikos JTA 兼容 XA 数据源的工厂示例。

基本示例:

public class MyEncryptedPasswordFactory extends BasicDataSourceFactory 

    @Override
    public Object getObjectInstance(Object obj, Name name, Context context, Hashtable<?, ?> environment)
            throws Exception 
        if (obj instanceof Reference) 
            Reference ref = (Reference) obj;
            DecryptPasswordUtil.replacePasswordWithDecrypted(ref, "password");
            return super.getObjectInstance(obj, name, context, environment);
         else 
            throw new IllegalArgumentException(
                    "Expecting javax.naming.Reference as object type not " + obj.getClass().getName());
        
    

Atomikos 示例:

public class MyEncryptedAtomikosPasswordFactory extends EnhancedTomcatAtomikosBeanFactory 
    @Override
    public Object getObjectInstance(Object obj, Name name, Context context, Hashtable<?, ?> environment)
            throws NamingException 
        if (obj instanceof Reference) 
            Reference ref = (Reference) obj;
            DecryptPasswordUtil.replacePasswordWithDecrypted(ref, "xaProperties.password");
            return super.getObjectInstance(obj, name, context, environment);
         else 
            throw new IllegalArgumentException(
                    "Expecting javax.naming.Reference as object type not " + obj.getClass().getName());
        
    

更新参考中的密码值:

public class DecryptPasswordUtil 

    public static void replacePasswordWithDecrypted(Reference reference, String passwordKey) 
        if(reference == null) 
            throw new IllegalArgumentException("Reference object must not be null");
        

        // Search for password addr and replace with decrypted
        for (int i = 0; i < reference.size(); i++) 
            RefAddr addr = reference.get(i);
            if (passwordKey.equals(addr.getType())) 
                if (addr.getContent() == null) 
                    throw new IllegalArgumentException("Password must not be null for key " + passwordKey);
                
                String decrypted = yourDecryptionMethod(addr.getContent().toString());
                reference.remove(i);
                reference.add(i, new StringRefAddr(passwordKey, decrypted));
                break;
            
        
    

一旦包含这些类的 .jar 文件位于 Tomcat 的类路径中,您就可以更新 server.xml 以使用它们。

<Resource factory="com.mycompany.MyEncryptedPasswordFactory" username="user" password="encryptedPassword" ...other options... />

<Resource factory="com.mycompany.MyEncryptedAtomikosPasswordFactory" type="com.atomikos.jdbc.AtomikosDataSourceBean" xaProperties.user="user" xaProperties.password="encryptedPassword" ...other options... />

【讨论】:

谢谢,这对我有用,而 Jerome 之前建议的方法没有(它在“super.getObjectInstance”上抛出异常,它似乎试图使用 un 访问数据源-解密密码)。作为一个小补充,我们使用会话数据库以及应用程序数据库;我发现要允许对会话数据库数据源进行密码加密,我们必须将包含自定义类的 jar 复制到 Tomcat /lib 目录中,并将它们包含在 Web 应用程序中,否则 Tomcat 会话无法访问它们经理。 正确。您可以将驱动程序文件放在我们使用 catalina.properties 文件向 Tomcat 公开的目录中。 我们是否必须为此在 Lib 文件夹中添加任何 Jars 文件? @greencheese 如果您提供了依赖项,则不必这样做。【参考方案4】:

Tomcat 需要知道如何连接数据库,所以它需要访问明文密码。如果密码是加密的,Tomcat 需要知道如何解密它,所以你只是将问题转移到其他地方。

真正的问题是:除了Tomcat,谁可以访问server.xml?一种解决方案是只将server.xml 的读取权限授予root 用户,要求Tomcat 以root 权限启动:如果恶意用户获得系统的root 权限,丢失数据库密码可能是一个小问题。

否则您应该在每次启动时手动输入密码,但这很少是可行的选择。

【讨论】:

到目前为止,我还不是安全专家,但在我看来,Tomcat 本身就是攻击的可能入口之一(通过 Tomcat 本身或其中一个库的漏洞利用)由应用程序使用,例如最近 Struts 2) 的问题,因此不应授予 root 权限!? 我同意。我不再使用 Tomcat,但我建议在没有 root 权限的情况下运行它。反正谁能读到tomcat目录,谁就能读到DB密码。据我所知,这是您使用的任何语言/框架都无法避免的问题。 这是一个糟糕的答案,为什么赞成票让我大吃一惊。保持安全。是的,如果可以的话,加密密码,不,你不必以 root 身份运行 tomcat(不仅那很糟糕,而且该语句做出了一个错误的假设,即你必须这样做)。访问系统、丢失密码或容易被发现是一个主要问题,因为在一段时间内可能无法检测到黑客攻击,并且您的数据库可能包含敏感数据。数据库是系统上的 GEM,也是有人试图破解您的盒子的主要原因。 只将 server.xml 的读取权限授予 root 用户是一种糟糕的做法。相反,应该创建一个具有运行 Tomcat 所需的最低权限的新用户。该用户应该只能启动/停止服务器。仅将所有配置文件的读取权限授予此用户。然后,禁用直接以该用户身份登录计算机。【参考方案5】:

经过 4 小时的工作,搜索问题和答案我得到了解决方案。 根据@Jerome Delattre 的回答,这里是完整的代码(带有 JNDI 数据源配置)。

上下文.xml

<Resource
    name="jdbc/myDataSource"
    auth="Container"
    type="javax.sql.DataSource"
    username="user"
    password="encryptedpassword"
    driverClassName="driverClass"
    factory="mypackage.MyCustomBasicDataSourceFactory"
    url="jdbc:blabla://..."/>

自定义数据源工厂:

package mypackage;

public class MyCustomBasicDataSourceFactory extends org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory 
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception 
        Object o = super.getObjectInstance(obj, name, nameCtx, environment);
        if (o != null) 
            BasicDataSource ds = (BasicDataSource) o;
            if (ds.getPassword() != null && ds.getPassword().length() > 0) 
                String pwd = MyPasswordUtilClass.unscramblePassword(ds.getPassword());
                ds.setPassword(pwd);
            
            return ds;
         else 
            return null;
        
    

数据源bean:

@Bean
public DataSource dataSource() 
    DataSource ds = null;
    JndiTemplate jndi = new JndiTemplate();
    try 
        ds = jndi.lookup("java:comp/env/jdbc/myDataSource", DataSource.class);
     catch (NamingException e) 
        log.error("NamingException for java:comp/env/jdbc/myDataSource", e);
    
    return ds;

【讨论】:

这是唯一的方法吗?是否可能取决于数据库供应商? @DavidBrossard nop,我在 postgresql 和 sql server 上使用相同的代码并且工作完美。【参考方案6】:

注意:

您可以使用WinDPAPI来加密和解密数据

public class MyDataSourceFactory extends DataSourceFactory

private static WinDPAPI winDPAPI;

protected static final String DATA_SOURCE_FACTORY_PROP_PASSWORD = "password";

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception

    Reference ref = (Reference) obj;
    for (int i = 0; i < ref.size(); i++) 
        RefAddr ra = ref.get(i);
        if (ra.getType().equals(DATA_SOURCE_FACTORY_PROP_PASSWORD)) 

            if (ra.getContent() != null && ra.getContent().toString().length() > 0) 
                String pwd = getUnprotectedData(ra.getContent().toString());
                ref.remove(i);
                ref.add(i, new StringRefAddr(DATA_SOURCE_FACTORY_PROP_PASSWORD, pwd));
            

            break;
        
    

    return super.getObjectInstance(obj, name, nameCtx, environment);
  

【讨论】:

【参考方案7】:

问题:正如所指出的,加密 context.xml 中的凭据,同时将解密密钥存储在下一个文件中,实际上只是解决了问题。由于访问 context.xml 的用户也需要访问解密密钥,因此如果应用程序或操作系统用户受到威胁,所有凭据仍然会受到威胁。

解决方案:唯一可以增加安全性的解决方案是从整个设置中完全删除解密密钥。这可以通过要求某人在启动时在您的应用程序中键入密码来实现,然后使用该密码来解密所有凭据。

进一步推迟解决方案:在大多数情况下,许多管理员和/或开发人员可能需要知道这样的密码。通过使用允许共享密码的密码共享解决方案(例如 1Password),安全性将延迟到每个管理员/开发人员用于解锁其个人密码库的个人主密码。

解决方案/讽刺的可能降级:使用此设置,最坏的情况是有人将他们的主密码简单地保存在附在监视器上的便签上。这是否比将解密密钥放在加密值旁边的文件中更安全可能应该是一个单独的 SO 问题,或者可能是未来的研究。

【讨论】:

【参考方案8】:

前面已经说过,如果您仍然想避免使用纯文本密码,您可以使用散列算法,例如 SHA-256 或(最好)SHA-512。创建密码时,获取哈希值并存储它而不是密码。当用户登录时,对密码进行哈希处理并查看它是否与存储的哈希密码匹配。 散列算法将字符串(或数字)从一个小字符串(或数字)空间转换成一个大得多的空间,这种方式的反转成本很高。

【讨论】:

这对于数据库连接完全没用 您的解决方案描述了在任何需要身份验证的系统(如数据库)中存储密码的通用解决方案。数据库本身可能已经在内部这样做了。 OP 要求的是一种在 tomcat 配置中以非明文方式存储密码的方法,使 Tomcat 能够使用该密码对数据库连接进行身份验证。【参考方案9】:

我们使用 C# 的 SHA1CryptoServiceProvider

print(SHA1CryptoServiceProvider sHA1Hasher = new SHA1CryptoServiceProvider();
        ASCIIEncoding enc = new ASCIIEncoding();

        byte[] arrbytHashValue = sHA1Hasher.ComputeHash(enc.GetBytes(clearTextPW));
        string HashData = System.BitConverter.ToString(arrbytHashValue);
        HashData = HashData.Replace("-", "");
        if (HashData == databaseHashedPassWO)
        
            return true;
        
        else
        
            return false;
        );

)

【讨论】:

不确定这与 tomcat 配置文件有什么关系。

以上是关于如何避免为 tomcat 的 server.xml 数据源的资源定义明文存储密码?的主要内容,如果未能解决你的问题,请参考以下文章

修改Tomcat默认端口号,避免与IDEA冲突

如何将apache tomcat的server.xml拆分成几个单独的文件?

如何配置tomcat中server.xml文件

配置Tomcat的server.xml编码为UTF-8时,再次发布项目,编码自动还原问题?

Tomcat源码分析——server.xml文件的加载与解析

Tomcat的server.xml配置讲解