1.数据的访问控制
我们先来看看哪些是经常访问数据库的用户?
软件程序(应用程序、数据库中间件)
人员:运维、开发、测试、产品、等
那接下来我们就来看看从这几点如何来控制数据库的访问。
1.1.软件程序层面
java、go、python或其他,已经有很丰富的orm和datasource框架或工具,我下面罗列一些java中常用jdbc连接池和orm框架以及数据库中间件| 名称 | 说明 | 是否有加解密策略 |
我相信很多程序的数据库连接与密码都是通过配置文件来保存的,假如应用服务器被黑客利用软件漏洞拿下,我相信通过部署的软件可以翻出数据库连接的配置,那么针对这一点我们如何有效的避免呢?
1.1.1.数据库连接密码加密
failover的时候应用程序可以不需要重启,只用重新创建连接即可。因此这层代理可以有效的防止数据库真实部署的机器被暴漏出去,起到了一定的安全作用。jdbc连接的时候往往是有密码访问的,我相信很多数据库的密码是明文的存储在配置文件中,虽然现在都用配置中心(configcenter)来统一管理应用的配置,如果使用明文来保存密码始终是无法规避泄漏的风险,因为应用程序始终要进行连接,在连接的时候要读取配置,不管配置是从云端同步下来还是从本地读取,只要是明文存储密码的就会存在安全问题。druid举个例子,具体的看看如何使用,也可以查看druid官方 示例(https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter)ConfigFilter为数据库密码提供加密功能<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"destroy-method="close">.................<!-- 如果fitlers走的配置中心,请去配置中心修改 --><property name="filters" value="${filters}" /><!-- 如果没有配置中心请直接修改 --><property name="filters" value="stat,config" /><!-- 以上两种filters配置2选一 --><property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${publickey}" />.................</bean>
filters=stat改为filters=stat,configjdbc.xxxx.password=123456改为jdbc.xxxx.password=加密后的值增加publickey=公钥
jdbc.password=p9i+fChqlaYnfhI+NoJqmrGwTyWwlFZ1W7Vi7i2MGZ8agFkGxGr/kWU//yDvPyXZ6YwJwnMKQ4zXpTZnfxWaRjfqWIRG+JzxSdSYEMp/bRCiIvzF6y8FdVCqN/0m0eQeZFvMCdIf4wqhKF0QRCEOTysZ3oGg7t5o35CIMpV1A5Y=jdbc连接池也都有类似的功能,但是不排除有一些没有这个功能的就需要我们自己动手开发来增强这部分功能。dbcp扩展这个功能。import java.sql.SQLFeatureNotSupportedException;import java.util.Properties;import java.util.logging.Logger;import org.apache.commons.dbcp.BasicDataSource;import org.slf4j.LoggerFactory;public class SecurityBasicDataSource extends BasicDataSource {private final org.slf4j.Logger logger = LoggerFactory.getLogger(SecurityBasicDataSource.class);@Overridepublic Logger getParentLogger() throws SQLFeatureNotSupportedException {throw new SQLFeatureNotSupportedException();}@Overridepublic void setPassword(String password) {try {//这里可以从任意地方读取数据库配置Properties p = ConfigLoaderUtils.loadConfig("jdbc.properties");String publickey = p.getProperty("publickey");//ConfigTools是实现私钥、公钥对加解密实现password = ConfigTools.decrypt(publickey, password);super.setPassword(password);} catch(Exception e) {logger.error("解密password出错", e);}}}
首先我们继承
dbcp数据源org.apache.commons.dbcp.BasicDataSource重写
setPassword设置密码的时候通过公钥和密文进行解密
dbcp扩展了数据库连接加解密的功能,是不是很简单。1.1.2.敏感数据加解密
AES或DES,为什么不使用非对称的公开密钥加密 ?DES作为示例,当然可以替换成任意的加解密算法。java的annotation可以帮助我们实现打标import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public @interface Cipher {}
@Cipher注释,说明这几个字段我们在保存、修改的时候需要加密,在查询的时候需要解密。CRUD的地方进行加解密调用这样会很傻很天真,作为被广泛使用的orm框架之一的mybatis这里我使用它作为示例讲解实现思路。mybatis提供拦截器机制,可以对执行的CRUD进行拦截处理操作,pagehelper 是一个分页的mybatis插件,就是利用拦截的机制来扩展分页功能。insert、update操作进行加密,对select操作进行解密,在mybatis的底层保存和修改都是update方法,查询都是query方法,刚好我们就对这两个方法进行拦截处理。import java.lang.reflect.Field;import java.util.List;import java.util.Properties;import org.apache.commons.lang3.StringUtils;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.plugin.Interceptor;import org.apache.ibatis.plugin.Intercepts;import org.apache.ibatis.plugin.Invocation;import org.apache.ibatis.plugin.Plugin;import org.apache.ibatis.plugin.Signature;import org.apache.ibatis.session.ResultHandler;import org.apache.ibatis.session.RowBounds;import org.apache.ibatis.session.defaults.DefaultSqlSession.StrictMap;import org.slf4j.Logger;import org.slf4j.LoggerFactory;@Intercepts({@Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,RowBounds.class, ResultHandler.class }) })public class CipherHelper implements Interceptor {private final Logger logger = LoggerFactory.getLogger(CipherHelper.class);/*** 加密密钥</br> 为null,使用默认密钥进行加解密</br>*/private String secureKey = null;/*** 是否允许宽容处理</br> 宽容处理的话,使用原值,反之throw {@link CipherException}</br>*/private boolean lenient = false;@Overridepublic Object intercept(Invocation invocation) throws Throwable {String methodName = invocation.getMethod().getName();if (methodName.equals("update") || methodName.equals("query")) {Object parameter = invocation.getArgs()[1];if (parameter instanceof List) {List<?> list = (List<?>) parameter;for (Object obj : list) {encrypt(obj);}} else if(parameter instanceof StrictMap) {StrictMap<?> strictMap = (StrictMap<?>) parameter;if (strictMap.containsKey("list")) {List<?> list = (List<?>) strictMap.get("list");for (Object obj : list) {encrypt(obj);}} else if (strictMap.containsKey("array")) {Object[] objects = (Object[]) strictMap.get("array");for (Object obj : objects) {encrypt(obj);}}} else {encrypt(parameter);}}Object returnValue = invocation.proceed();if (methodName.equals("query")) {if (returnValue instanceof List) {List<?> list = (List<?>) returnValue;for (Object obj : list) {decrypt(obj);}} else {decrypt(returnValue);}}return returnValue;}/*** 加密处理** @param parameter* @throws IllegalAccessException*/private void encrypt(Object parameter) throws IllegalAccessException {if (parameter == null) return;Class<?> clazz = parameter.getClass();if (!clazz.getSimpleName().endsWith("Entity")) {return;}for (; clazz != Object.class; clazz = clazz.getSuperclass()) {Field[] fields = clazz.getDeclaredFields();for (int i = 0; i < fields.length; i++) {if (!fields[i].isAnnotationPresent(Cipher.class)) {continue;}if (!fields[i].getType().equals(String.class)) {logger.debug("加密字段只支持String类型,当前类型非String,跳过!");continue;}fields[i].setAccessible(true);String v = (String) fields[i].get(parameter);if (StringUtils.isBlank(v)) {logger.debug("加密字段值为null,跳过!");continue;}try {String crypt = DESTools.encrypt(secureKey, v);fields[i].set(parameter, crypt);logger.debug("加密处理字段,{}", fields[i].getName());} catch (Exception e) {if (lenient) {logger.warn("加密处理失败,宽容处理使用原值");} else {throw new CipherException("加密处理失败,不允许宽容处理["+v+"]", e);}}}}}/*** 解密处理** @param obj* @throws IllegalAccessException* @throws Exception*/private void decrypt(Object obj) throws IllegalAccessException, Exception {if (obj == null) return;Class<?> clazz = obj.getClass();if (!clazz.getSimpleName().endsWith("Entity")) {return;}for (; clazz != Object.class; clazz = clazz.getSuperclass()) {Field[] fields = clazz.getDeclaredFields();for (int i = 0; i < fields.length; i++) {if (!fields[i].isAnnotationPresent(Cipher.class)) {continue;}if (!fields[i].getType().equals(String.class)) {logger.debug("解密字段只支持String类型,当前类型非String,跳过!");continue;}fields[i].setAccessible(true);String v = (String) fields[i].get(obj);if (StringUtils.isBlank(v)) {logger.debug("解密字段值为null,跳过!");continue;}try {String crypt = DESTools.decrypt(secureKey, v);fields[i].set(obj, crypt);logger.info("解密处理字段,{}", fields[i].getName());} catch (Exception e) {if (lenient) {logger.warn("解密处理失败,宽容处理使用原值");} else {throw new CipherException("解密处理失败,不允许宽容处理["+v+"]", e);}}}}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {if (properties != null && StringUtils.isNotBlank(properties.getProperty("secureKey"))) {this.secureKey = properties.getProperty("secureKey");}if (properties != null && StringUtils.isNoneBlank(properties.getProperty("lenient"))) {this.lenient = Boolean.parseBoolean(properties.getProperty("lenient"));}}}
mybatis的插件扩展机制在执行过程进行拦截处理,plugin方法是插件的装载方法,setProperties方法设置关键属性,比如说密钥串。encrypt:是加密方法,这里加密方法需要注意的是,mybatis参数支持Pojo 、Map、StrictMap、List、Array,我们使用注解@Cipher是用在类上的所以只对Pojo生效,如果是Map它天生的key,value格式无法支持打标,我们这里对Map类型进行跳过不处理,如果非要处理Map也是有办法的,需要固定加解密的key值,对特定的key进行识别并加解密替换value,加密方法通过查找有注解@Cipher的字段进行加密并且回填值。decrypt:是解密方法,主要用在查询时的解密,这里需要注意的是查询有可能返回特定的Pojo也可能返回List,所以这里解密的时候需要根据类型来分别处理,如果是List需要进行很层次查找,如果是Pojo那就查找使用注解@Cipher的字段进行解密并且回填值。intercept:是拦截方法,在update、query前后进行拦截处理,在这个方法里需要进行如下步骤:识别当前执行的
method是update还是query如果是
update那就进行加密如果是
query那就进行解密识别参数类型是
List、StrictMap、Pojo如果
StrictMap里是list那就循环调用encrypt方法如果
StrictMap里是array那就循环调用encrypt方法如果
List里是Pojo那就循环调用encrypt方法如果
List里是Map跳过处理,或者使用上面我们说的识别某些固定key进行加密处理如果是
List需要再深层次看一下List里是什么类型,这里建议使用递归方式如果是
StrictMap需要再深层次看一下StrictMap里是什么类型,这里建议使用递归方式如果是
Pojo那就调用encrypt方法执行sql处理获取返回值
获取返回值并且执行的方法是
query时,进行解密处理如果是
List深层次查找内部类型,这里建议使用递归方式如果是
Pojo那就调用decrypt方法如果
List里是Pojo那就循环调用decrypt方法如果
List里是Map跳过处理,或者使用上面我们说的识别某些固定key进行解密处理识别返回的类型是
List还是Pojo
1.2.人员层面
前面我们说了绝大多数的数据泄密都不是技术问题而是人员管理问题,我们要对人员进行有效的管理与控制。
1.2.1.开发或测试人员
这类人一般对数据是有CRUD的诉求,针对这类人员的控制有如下几个方面
开发人员只能连接测试环境数据库,不允许连接生产数据库,即使连接vpn也不行。
开发人员申请数据库需要走运维工单流程,运维提供数据库连接密码时应直接提供密文,或者运维直接给配置到配置中心。
配置文件或配置中心禁止存储明文密码,需要对jdbc等其他敏感密码进行脱敏处理。
生产服务器需要通过跳板机访问,禁止开发使用
root直接操作,如果看应用日志可以走日志平台,实在没有日志平台可以给跳板机开通app用户只给查看固定目录日志的权限,如果要发布走devops平台,如果没有可以提供给运维进行发布。查询生产数据走
dms平台,对敏感信息进行脱敏或隐藏,对上线的sql和日常的查询日志做到dms可管控。微信搜索公众号:Linux技术迷,回复:linux 领取资料 。
提交到开放环境时需要注意以下几点
提交到开放的仓库(
github、gitlab、gitee等),需要对代码进行审核,避免有hardcode的公司服务器密码、ip、端口、密钥等。提交到开放的论坛(
csdn、oschina、知乎、公众号、社区分享等),需要对文章进行审核,避免有不允许公开的技术细节或敏感信息。
1.2.2.运维或DBA人员
这类人一般操作权限都很高,出问题概率最高的人员,有很多删库跑路或误操作rm -rf的例子哈哈哈!所以这类人更要重点管控。
需要搭建和处理运维工单平台,用于开发提出的运维资源申请,尤其是数据库密码,直接提供加密后的密文和公钥。
需要搭建和处理数据库管理工具
dms,用于开发日常生产数据查询和发版时SQL升级。需要提供跳板机和给跳板机提供不同等级的用户,提供给特别需要的人访问生产环境机器。
需要提供
devops平台或者自动化发版工具,避免手动操作失误带来问题,对开发提供升级发布的流水线。对服务器密码需要进行加密存储,可以借助密码管理工具。
运维最好也不要使用
root用户操作服务器,使用特定权限的用户操作。dba最好也不要使用root用户操作数据库,使用特定权限的用户操作。制定责任人机制,对应的责任项必须到具体人,具体可以参考 责任分配矩阵RAM 。
关键重要的操作需要至少两个人在场,具体可以参考 责任分配矩阵RAM 。
1.2.3.产品或业务人员
这类人一般对数据有查询和分析的诉求,有分析诉求就需要导出数据,所以分析诉求统一走公司BI工具,也有少部分有修改的诉求。
查询分析数据,统一接入
BI工具,并且BI工具需要有功能和数据权限,并对敏感数据导出加以控制,导出走审批并脱敏。提交数据变更,统一接入
dms平台。产品的分析文件(word、excel、ppt)应该进行加密,这种一般依赖公司引入文档安全的解决方案,要花钱的,如果不想花钱那就没啥好办法。
2.总结
如喜欢本文,请点击右上角,把文章分享到朋友圈
作者:凝雨
来源:https://ningyu1.github.io/20201229/datasource-security.html
版权申明:内容来源网络,仅供分享学习,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
我们都是黑客