mongoDB加密小记

于由astupidcoder发布

数据库加密有两个方面,一个是授权登录,即未经授权的人不能登录相关的数据库;第二个是通信加密,防止被抓包网络通信获得数据。mongoDB在这俩方面都有很好的支持。鉴于mongoDB未加密导致的数据泄露新闻每天都有,刚好最近公司业务有相关需要,于是小总结了一下,如下:

授权登录

mongoDB官方文档:授权登录

首先,了解一下mongoDB各种级别的权限:

mongoDB官方文档:roles级别

详细内容在这里就不搬运了,大概就是:

  1. 用户级别的roles有:read和readWrite两种,可以加到Collection级别;
  2. admin级别的roles比较复杂,有dbAdmin(其实并不是完全的管理员权限,比如不能给其他用户授权等),dbOwner(数据库拥有者,对该数据库具有完全权限),userAdmin(管理Role,可以给自己授权任意权限,所以其实间接拥有了对该数据库的所有操作权限)
  3. 集群Administration roles,这个没仔细看,等以后有需要了再说吧。
  4. Backup and Restoration Roles。备份和恢复权限。

  5. 及于所有数据库的相关权限:readAnyDatabase(见上面的read权限),readWriteAnyDatabase(见上面的readWrite权限),userAdminAnyDatabase(见上面的userAdmin权限),dbAdminAnyDatabase(见上面的dbAdmin权限)

  6. 超级权限:root。oneAboveAll,上帝权限。

为了演示方便,我以上帝用户进行接下来的操作。

开启数据库授权登录示例

  1. 安装数据库,可以本机安装,也可以docker。不赘述,以下以docker安装为例。
    docker pull mongo:latest
    
  2. 以验证权限开启数据库:
    docker run -d --name mongoDB -p 27017:27017 mongo mongod --auth
    

    这样就可以开启授权登录,但在命令行直接这么启动,可能会造成下次启动时忘记加auth,数据库回到未保护状态,因此比较好的方式是直接把auth加到配置文件里,每次运行时都会开启授权登录。如果在系统里直接安装的mongoDB,那么配置文件在/etc/mongod.conf中,在里面加上下面这两行:

    security:
     authorization: enabled
    

    之后重启mongo: sudo service mongod restart即可。在docker里也可以docker exec进入运行中容器,修改相应的文件。但更好的办法还是自己建立一个docker镜像。

    mkdir ~/mongo
    cd ~/mongo
    vim mongod.conf
    #################### 粘贴下面代码
    # Where and how to store data.
    storage:
     dbPath: /var/lib/mongodb
     journal:
       enabled: true
    # where to write logging data.
    systemLog:
     destination: file
     logAppend: true
     path: /var/log/mongodb/mongod.log
    
    # network interfaces
    net:
     port: 27017
     bindIp: 0.0.0.0
    
    # how the process runs
    processManagement:
     timeZoneInfo: /usr/share/zoneinfo
    
    #security:
    security:
     authorization: enabled
    #################### 粘贴完毕 :q退出vim
    vim Dockerfile
    #################### 把下面代码粘贴进去
    FROM mongo:latest
    RUN mkdir -p /mongo && mkdir -p /var/lib/mongodb && chown -R mongodb:mongodb /mongo /var/lib/mongodb
    COPY ./ /mongo
    ENTRYPOINT ["docker-entrypoint.sh"]
    EXPOSE 27017
    CMD ["mongod","-f","/mongo/mongod.conf"]
    #################### 粘贴完毕 :q退出vim
    sudo docker build -t mongo:mywithauth .
    

    即可得到默认开启权限验证的数据库。

  3. 启动数据库后,进入数据库创建管理员账户,仍旧以docker为例。

    docker run -d --name mongoDB -p 27017:27017 mongo:mywithauth
    docker exec -it mongoDB bash
    ################# 以下操作是在docker容器中进行
    mongo
    ################# 以下操作是在mongo shell中进行
    use admin;
    db.createUser( { "user" : "root",
                   "pwd": "root","roles" : 
                   [ { role: "root", db: "admin" } ] 
                  } )
    #### 在这里可以看到createUser的格式,user,password,以及权限,权限是个数组,可以有多个
    #### 我们这里直接创建了一个账号密码都为root的上帝管理员角色
    exit
    #### 退出mongoShell重新进
    mongo
    ################# 以下操作是在mongo shell中进行
    use admin
    db.auth('root','root')
    #### 返回1则表示权限认证成功。
    
  4. 至此,权限控制的演示就结束了。如果需要创建其他用户,只要用已创建的超级管理员登陆后创建即可。

  5. 以下附上python中类似操作的示例代码:

程序内连接权限验证示例

python

python
# 首先必须用admin账户登录
client = MongoClient('mongodb://root:root@127.0.0.1:27017')
db = client["testdb"]
# 新建的账户权限只及于这个db,这一步只是在admin数据库中增加了一条user信息而已
db.add_user('username',password='password',roles=['readWrite'])
col = db['collectionName']
col.insert_one({'id':10}) # 到这一步db和col才真正建立起来
# 此时用户名username,密码password的用户拥有testdb数据库的readWrite权限。

需要注意的是,如果使用只有单数据库操作权限的用户登录mongo,那么python中的登录URL应该写成:

python
client = MongoClient('mongodb://username:password@127.0.0.1:27017/testdb')

否则会出现权限验证失败。Pymongo官方文档

java

java
String mongoBaseUrl = "127.0.0.1:27017";
String username = "username";
String password = "password";
String dbName = "testdb"
return new MongoClient(new ServerAddress(mongoBaseUrl),
MongoCredential.createScramSha1Credential(username, dbName,
IOUtils.toCharArray(new StringReader(password))),
MongoClientOptions.builder().build());

pom文件中引入下列包:

xml
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>3.7.1</version>
</dependency>

如果使用了Spring boot,因为Spring boot的自动配置功能,启动时会报一个不影响启动的异常,可以在在主类上加以下注解来避免:

java
@SpringBootApplication(exclude = {
MongoAutoConfiguration.class
})

SSL通信加密

如果只开启访问控制而不添加SSL加密,实际上是防君子不防小人,wireShark抓包就能获得mongo客户端与服务器端的明文通信。官方文档也提醒,如果开启权限控制,就应该同时开启SSL。

开启SSL通信也包含两个部分,一个是数据库服务端开启SSL,一个是客户端开启SSL。

  1. 需要创建证书,我们使用openssl生成,同时为生成ssl加密mongo的docker镜像做准备。

    “`shell

    cd ~/mongo
    mkdir cert
    cd cert
    sudo openssl req -newkey rsa:2048 -new -x509 -days 365 -nodes -out client-cert.crt -keyout client-cert.key
    Generating a RSA private key
    …………………………………………………………………………………………………………….+++++
    ……+++++
    writing new private key to ‘client-cert.key’


You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter ‘.’, the field will be left blank.


Country Name (2 letter code) [AU]:ZH
State or Province Name (full name) [Some-State]:a
Locality Name (eg, city) []:a
Organization Name (eg, company) [Internet Widgits Pty Ltd]:a
Organizational Unit Name (eg, section) []:a
Common Name (e.g. server FQDN or YOUR name) []:mongo-server # 注意这一行,是有意义的
Email Address []:example@quit.com

sudo cat client-cert.key client-cert.crt > client.pem
sudo openssl req -newkey rsa:2048 -new -x509 -days 365 -nodes -out mongodb-cert.crt -keyout mongodb-cert.key
### 和生成上个key一样的输出,仍旧记得Common Name那一行要写mongo-server
sudo cat mongodb-cert.key mongodb-cert.crt > mongodb.pem
“`

解释一下mongo-server的意义,mongo在这里使用的SSL其实是类似于HTTPS的那种,带域名校验的,官方文档里这么解释:

  • The mongo shell verifies that the certificate is from the specified Certificate Authority (–sslCAFile). If the certificate is not from the specified CA, the mongo shell will fail to connect.

  • The mongo shell verifies that the hostname (specified in –host option or the connection string) matches the SAN (or, if SAN is not present, the CN) in the certificate presented by the mongod or mongos. If SAN is present, mongo does not match against the CN. If the hostname does not match the SAN (or CN), the mongo shell will fail to connect.

    To connect a mongo shell to a mongod or mongos that requires TLS/SSL, specify the –host option or use a connection string.

提炼要点就是,我们在上面填写的Common Name (e.g. server FQDN or YOUR name),就是这里要验证的SAN(其实是CN,Commons Name),也就是说,所有访问该mongo的客户端,都必须用mongo-server这个host来连接,不然SSL验证就会失败。

比如说,我们部署在本地的mongo,要访问的话必须用mongodb://mongo-server:27017访问,同时在/etc/hosts中添加一条映射记录127.0.0.1 mongo-server,使mongo-server被解析为127.0.0.1。

  1. 创建docker镜像做准备,修改mongod.conf
    > rm mongod.conf
    > vim mongod.conf
    ############### 粘贴以下代码进去
    # Where and how to store data.
    storage:
     dbPath: /var/lib/mongodb
     journal:
       enabled: true
    #  engine:
    #  mmapv1:
    #  wiredTiger:
    
    # where to write logging data.
    systemLog:
     destination: file
     logAppend: true
     path: /var/log/mongodb/mongod.log
    
    # network interfaces
    net:
     port: 27017
     bindIp: 0.0.0.0
     ssl:
       mode: requireSSL
       PEMKeyFile: /mongo/cert/mongodb.pem
       CAFile: /mongo/cert/client.pem
    
    # how the process runs
    processManagement:
     timeZoneInfo: /usr/share/zoneinfo
    
    #security:
    security:
     authorization: enabled
    

    修改Dockerfile:

    > rm Dockerfile
    > vim Dokcerfile
    FROM mongo:latest
    RUN mkdir -p /mongo && mkdir -p /var/lib/mongodb && chown -R mongodb:mongodb /mongo /var/lib/mongodb
    COPY ./ /mongo
    ENTRYPOINT ["docker-entrypoint.sh"]
    EXPOSE 27017
    CMD echo "127.0.0.1 mongo-server" >> /etc/hosts; mongod -f /mongo/mongod.conf
    

    之后build:

    sudo docker build -t mongo:authssl .
    

    run起来docker:

    docker run -d --name mongoDBSSL -p 27017:27017 mongo:authssl
    docker exec -it mongoDBSSL /bin/bash
    mongo --host mongo-server:27017 --ssl --sslPEMKeyFile /mongo/cert/client.pem --sslCAFile /mongo/cert/mongodb.pem
    

    随后就登陆到mongoShell了,我们先根据上面权限控制那一套,加上root用户,就可以用了。

程序连接ssl mongo示例

和上面一样,先在/etc/hosts中添加一行127.0.0.1 mongo-server

python
client = pymongo.MongoClient('mongodb://root:root@mongo-server:27017',
                             ssl=True,
                             ssl_ca_certs='/Users/mt/mongo/cert/mongodb.pem',
                             ssl_certfile='/Users/mt/mongo/cert/client.pem')
java

需要在shell上先做配置。

> cd ~/mongo/cert
> openssl pkcs12 -export -out mongodb.pkcs12 -in mongodb.pem
# 这里提示输入密码,两次。记住,以后要用,假设是password1
> openssl pkcs12 -export -out client.pkcs12 -in client.pem
# 这里提示输入密码,两次。记住,以后要用,假设是password2
System.setProperty("javax.net.ssl.trustStore", "/path/to/mongodb.pkcs12");
System.setProperty("javax.net.ssl.trustStorePassword", "password1");
System.setProperty("javax.net.ssl.keyStore", "/path/to/client.pkcs12");
System.setProperty("javax.net.ssl.keyStorePassword", "password2");

MongoClientOptions options = new MongoClientOptions.Builder()
        .sslEnabled(true).build();

MongoClient mongoClient = new MongoClient(new ServerAddress("mongo-server:27017"),
        MongoCredential.createScramSha1Credential("root", "admin",
                IOUtils.toCharArray(new StringReader("root"))),
        options);

MongoDatabase db = mongoClient.getDatabase("admin");
for (String listCollectionName : db.listCollectionNames()) {
    System.out.println(listCollectionName);
}

坑和方法论

自从接触docker以后,就不怎么喜欢在本机装各种测试性软件,担心污染环境。所以在测试过程中踩了很多docker的坑。mongo的坑反而比较少。

  1. mongo的官方docker镜像,其运行mongodb的用户并不是root,而是mongodb,但使用docker -v参数挂载到docker里面的目录,其文件所属用户是root,这就导致了一些微妙的权限问题,导致类似key无法读取之类的错误发生。但如果不看官方的Dockerfile的话,极难发现这个权限问题。

    总结:在想要定制docker镜像时,有必要查看Dockerfile

  2. mongo的官方镜像的/etc/下有一个mongod.conf.orig文件,经查看官方的Dockerfile,发现是从/etc/mongo.conf改名得到的,因此,可知这个文件就是原来应该使用的conf文件,但docker镜像改了名,默认不读取这个conf。而我们这里显然是要应用conf文件,于是为了测试,我直接运行了下面的命令:

    docker run -it --name mongoconf mongo:latest --config /etc/mongod.conf.orig
    

    按理说,—it会进入交互画面,也就是会打印出log,但这一次什么输出都没有,直接退出了。于是现在陷入了一个尴尬的境地:

  • 为什么没有log了?
  • docker镜像已经停止了,我也无法exec进去进行尸检了。

    后来查了查,得到一个办法,直接把一个container commit成一个image,这样就保存了案发现场。于是docker commit mongoconf mongo:dead,再docker run —it mongo:dead bash,进入docker容器内部。find / -name 'log' | grep mongo,找到了log文件,查看了一下。发现这个conf文件中指定存放mongo数据的目录是/var/lib/mongodb,而镜像的Dockerfille文件修改了这个路径,删除了/var/lib/mongodb,所以启动时就失败了。而—it没有打印出任何日志的原因,是/etc/mongod.conf.orig这个配置文件指定log写入文件了。

    所以我们在构建Dockerfile时,手动创建了/var/lib/mongodb目录,并且将其owner修改为mongodb。

    总结:docker commit保存死去的docker的案发现场很好用

  1. docker镜像中对hosts文件的修改,将在下一次启动时被抹除。因为我们的mongo配上了ssl以后,需要使用mongo-server这个host访问数据库,否则就会拒绝连接。因此,最早我很天真地把Dockerfile写成了这样:

    FROM mongo:latest
    RUN mkdir -p /mongo && mkdir -p /var/lib/mongodb && chown -R mongodb:mongodb /mongo /var/lib/mongodb
    RUN echo "127.0.0.1 mongo-server" >> /etc/hosts # 重点这一行
    COPY ./ /mongo
    ENTRYPOINT ["docker-entrypoint.sh"]
    EXPOSE 27017
    CMD echo "127.0.0.1 mongo-server" >> /etc/hosts; mongod -f /mongo/mongod.conf
    

    然而启动docker之后,会发现hosts文件中并没有127.0.0.1 mongo-server这一行。于是去搜索了一下,原来host文件是docker每次生成后,通过挂载机制默认挂载到镜像中去的,因为docker每次生成新的container时,ip地址都会发生变化,特别是用docker-compose时,可能要在hosts文件中加不少记录。因此每次生成hosts文件是合理的。

    那么我们的这个需求怎么做呢?

    这就涉及到docker的启动机制。上面Dockerfile中有两行对于理解docker的启动非常重要:

    ENTRYPOINT ["docker-entrypoint.sh"]
    CMD echo "127.0.0.1 mongo-server" >> /etc/hosts; mongod -f /mongo/mongod.conf
    

    首先,docker启动时,一定会执行ENTRYPOINT参数指定的脚本,并将CMD的命令附在该脚本后,作为该脚本的参数传入脚本。当我们在docker run的镜像名后添加命令时,这些命令会替代CMD指定的命令传入脚本。假如没有指定ENTRYPOINT,那docker启动时就会使用CMD中指定的命令,如果docker run传入了参数,就使用传入的参数替换CMD的参数。而这一步是发生在整个docker container构建完成,马上要启动的那一步的。

    了解到这一步也就基本有了解法,我们直接把Dockerfile改成这个样子就OK了。

  2. mongo的SSL在docker镜像内验证成功后,无法用mongo3t登陆。在最初的conf文件里,我是这么写的:

    net:
     port: 27017
     # bindIp: 127.0.0.1
     ssl:
       mode: requireSSL
       PEMKeyFile: /mongo/cert/mongodb.pem
       CAFile: /mongo/cert/client.pem
    

    当时我以为注销到这一句,mongo就会自动对所有ip发来的请求开放,所以3t连接不上我一直很疑惑,这时候我还没有想到是这个bindIp参数的锅,以为是3T哪里连接方式有问题,这时候就想到只能控制变量来搞了。于是我又run了一个mongo:authssl镜像。

    docker run --link mongoDBSSL:mongo-server -it --rm mongo:authssl bash
    

    进入后用完全一样的命令来连接mongo:

    mongo --host mongo-server:27017 --ssl --sslPEMKeyFile /mongo/cert/client.pem --sslCAFile /mongo/cert/mongodb.pem
    

    直接拒绝连接。这时候在docker exec -it mongoDBSSL bash仍然可以用同样的命令连接。这时候就猜出来大概是bindIP的问题了。于是修改之,问题得解。

    总结:遇到问题,一定要先控制变量,这样才能迅速把问题缩小到一个范围,帮助解决。

总之,还有一个体会,就是当你的知识越丰富,就越容易触类旁通,发现问题,也就有更多的思路和工具去解决问题。比如不知道docker是由Dockerfile构建的,就很难发现那个权限问题,如果不知道docker的—link命令,就没有很好地手段去控制变量,只能一遍一遍试,瞎猜。

重要的永远都是解决问题的具体办法,而是解决问题的思路。


0 条评论

发表回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据。