前言
对于任何一个企业来说,其数据库系统中所保存数据的安全性无疑是非常重要的,尤其是公司的有些商业数据,可能数据就是公司的根本,失去了数据的安全性,可能就是失去了公司的一切。本章将针对 MySQL 的安全相关内容进行较为详细的介绍。
4.1 数据库系统安全相关因素
一、外围网络:
MySQL的大部分应用场景都是基于网络环境的,而网络本身是一个充满各种入侵危险的环境,所以要保护他的安全,在条件允许的情况下,就应该从最外围的网络环境开始“布防”,因为这一层防线可以从最大范围内阻止可能存在的威胁。
在网络环境中,任意两点之间都可能存在无穷无尽的“道路”可以抵达,是一个真正“条条道路通罗马”的环境。在那许许多多的道路中,只要有一条道路不够安全,就可能被入侵者利用。当然,由于所处的环境不同,潜在威胁的来源也会不一样。有些MySQL所处环境是暴露在整个广域网中,可以说是完全“裸露”在任何可以接入网络环境的潜在威胁者面前。而有些MySQL是在一个环境相对小一些的局域网之内,相对来说,潜在威胁者也会少很多。处在局域网之内的MySQL,由于有局域网出入口的网络设备的基本保护,相对于暴露在广域网中要安全不少,主要威胁对象基本上控制在了可以接入局域网的内部潜在威胁者,和极少数能够突破最外围防线(局域网出入口的安全设备)的入侵者。所以,尽可能的让我们的MySQL处在一个有保护的局域网之中,是非常必要的。
二、主机:
有了网络设备的保护,我们的MySQL就足够安全了么?我想大家都会给出否定的回答。因为即使我们局域网出入口的安全设备足够的强大,可以拦截住外围试图入侵的所有威胁者,但如果威胁来自局域网内部呢?比如局域网中可能存在被控制的设备,某些被控制的有权限接入局域网的设备,以及内部入侵者等都仍然是威胁者。所以说,吉使在第一层防线之内,我们仍然存在安全风险,局域网内部仍然会有不少的潜在威胁存在。
这个时候就需要我们部署第二道防线“主机层防线”了。“主机层防线”主要拦截网络(包括局域网内)或者直连的未授权用户试图入侵主机的行为。因为一个恶意入侵者在登录到主机之后,可能通过某些软件程序窃取到那些自身安全设置不够健壮的数据库系统的登入口令,从而达到窃取或者破坏数据的目的。如一个主机用户可以通过一个未删除且未设置密码的无用户名本地帐户轻易登入数据库,也可以通过MySQL初始安装好之后就存在的无密码的“root@localhost”用户登录数据库并获得数据库最高控制权限。
非法用户除了通过登入数据库获取(或者破坏)数据之外,还可能通过主机上面相关权限设置的漏洞,跳过数据库而直接获取MySQL数据(或者日志)文件达到窃取数据的目的,或者直接删除数据(或者日志)文件达到破坏数据的目的。
三、数据库:
通过第二道防线“主机层防线”的把守,我们又可以挡住很大一部分安全威胁者。但仍然可能有极少数突破防线的入侵者。而且即使没有任何“漏网之鱼”,那些有主机登入权限的使用者呢?是否真的就是完全可信任对象?No,我们不能轻易冒这个潜在风险。对于一个有足够安全意识的管理员来说,是不会轻易放任任何一个潜在风险存在的。
这个时候,我们的第三道防线,“数据库防线”就需要发挥他的作用了。“数据库防线”也就是MySQL数据库系统自身的访问控制授权管理相关模块。这道防线基本上可以说是MySQL的最后一道防线了,也是最核心最重要的防线。他首先需要能够抵挡住在之前的两层防线都没有能够阻拦住的所有入侵威胁,同时还要能够限制住拥有之前二层防线自由出入但不具备数据库访问权限的潜在威胁者,以确保数据库自身的安全以及所保存数据的安全。
之前的二层防线对于所有数据库系统来说基本上区别不大,都存在着基本相同的各种威胁,不论是Oracle还是MySQL,以及任何其他的数据库管理系统,都需要基本一致的“布防”策略。但是这第三层防线,也就是各自自身的“数据库防线”对于每个数据库系统来说都存在较大的差异,因为每种数据库都有各自不太一样的专门负责访问授权相关功能的模块。不论是权限划分还是实现方式都可能不太一样。
对于MySQL来说,其访问授权相关模块主要是由两部分组成。一个是基本的用户管理模块,另一个是访问授权控制模块。用户管理模块的功能相对简单一些,主要是负责用户登录连接相关的基本权限控制,但其在安全控制方面的作用却不比任何环节小。他就像MySQL的一个“大门门卫”一样,通过校验每一位敲门者所给的进门“暗号”(登入口令),决定是否给敲门者开门。而访问授权控制模块则是随时随地检查已经进门的访问者,校验他们是否有访问所发出请求需要访问的数据的权限。通过校验者可顺利拿到数据,而未通过校验的访问者,只能收到“访问越权了”的相关反馈。
上面的三道防线组成了如图4-1所示的三道坚固的安全保护壁垒,就像三道坚固的城墙一样保护这 MySQL 数据库中的数据。只要保障足够,基本很难有人能够攻破这三道防线。
图4-1
四、代码:
1、SQL语句相关安全因素:
“SQL注入攻击”这个术语我想大部分读者朋友都听说过了?指的就是攻击者根据数据库的SQL语句解析器的原理,利用程序中对客户端所提交数据的校验漏洞,从而通过程序动态提交数据接口提交非法数据,达到攻击者的入侵目的。
“SQL注入攻击”的破坏性非常的大,轻者造成数据被窃取,重者数据遭到破坏,甚至可能丢失全部的数据。如果读者朋友还不是太清楚何为“SQL 注入攻击”,建议通过互联网搜索一下,可以得到非常多非常详细的介绍及案例分析,这里有不做详细介绍了。
2、程序代码相关安全因素:
程序代码如果权限校验不够仔细而存在安全漏洞,则同样可能会被入侵者利用,达到窃取数据等目的。比如,一个存在安全漏洞的信息管理系统,很容易就可能窃取到其他一些系统的登入口令。之后,就能堂而皇之的轻松登录相关系统达到窃取相关数据的目的。甚至还可能通过应用系统中保存不善的数据库系统连接登录口令,从而带来更大的损失。
4.2 MySQL 权限系统介绍
4.2.1 权限系统简介
MySQL的权限系统在实现上比较简单,相关权限信息主要存储在几个被称为grant tables的系统表中,即:mysql.User,mysql.db,mysql.Host,mysql.table_priv和mysql.column_priv。由于权限信息数据量比较小,而且访问又非常频繁,所以Mysql在启动的时候,就会将所有的权限信息都Load到内存中保存在几个特定的结构中。所以才有我们每次手工修改了权限相关的表之后,都需要执行“FLUSH PRIVILEGES”命令重新加载MySQL的权限信息。当然,如果我们通过GRANT,REVOKE或者DROP USER命令来修改相关权限,则不需要手工执行FLUSH PRIVILEGES命令,因为通过GRANT,REVOKE或者DROP USER命令所做的权限修改在修改系统表的同时也会更新内存结构中的权限信息。在MySQL5.0.2或更高版本的时候,MySQL还增加了CREATE USER命令,以此创建无任何特别权限(仅拥有初始USAGE权限)的用户,通过CREATE USER命令创建新了新用户之后,新用户的信息也会自动更新到内存结构中。所以,建议读者一般情况下尽量使用GRANT,REVOKE,CREATE USER以及DROP USER命令来进行用户和权限的变更操作,尽量减少直接修改grant tables来实现用户和权限变更的操作。
4.2.2 权限授予与去除
要为某个用户授权,可以使用GRANT命令,要去除某个用户已有的权限则使用REVOKE命令。当然,出了这两者之外还有一种比较暴力的办法,那就是直接更新grant tables系统表。当给某个用户授权的时候,不仅需要指定用户名,同时还要指定来访主机。如果在授权的时候仅指定用户名,则MySQL会自动认为是对'username'@'%'授权。要去除某个用户的的权限同样也需要指定来访主机。
可能有些时候我们还会需要查看某个用户目前拥有的权限,这可以通过两个方式实现,首先是通过执行“SHOW GRANTS FOR 'username'@'hostname'” 命令来获取之前该用户身上的所有授权。另一种方法是查询grant tables里面的权限信息。
4.2.3 权限级别
MySQL中的权限分为五个级别,分别如下:
1、Global Level:
Global Level的权限控制又称为全局权限控制,所有权限信息都保存在mysql.user表中。Global Level的所有权限都是针对整个mysqld的,对所有的数据库下的所有表及所有字段都有效。如果一个权限是以Global Level来授予的,则会覆盖其他所有级别的相同权限设置。比如我们首先给abc用户授权可以UPDATE 指定数据库如test的t表,然后又在全局级别REVOKE掉了abc用户对所有数据库的所有表的UPDATE权限。则这时候的abc用户将不再拥有用对test.t表的更新权限。Global Level主要有如下这些权限(见表4-1):
表 4-1
名称 | 版本支持 | 限制信息 |
ALTER | ALL | 表结构更改权限 |
ALTER ROUTINE | 5.0.3+ | procedure,function和trigger等的变更权限 |
CREATE | ALL | 数据库,表和索引的创建权限 |
CREATE ROUTINE | 5.0.3+ | procedure,function和trigger等的变更权限 |
CREATE TEMPORARY TABLES | 4.0.2+ | 临时表的创建权限 |
CREATE USER | 5.0.3+ | 创建用户的权限 |
CREATE VIEW | 5.0.1+ | 创建视图的权限 |
DELETE | All | 删除表数据的权限 |
DROP | All | 删除数据库对象的权限 |
EXECUTE | 5.0.3+ | procedure,function和trigger等的执行权限 |
FILE | All | 执行LOAD DATA INFILE 和 SELECT ... INTO FILE的权限 |
INDEX | All | 在已有表上创建索引的权限 |
INSERT | All | 数据插入权限 |
LOCK TABLES | 4.0.2+ | 执行LOCK TABLES命令显示给表加锁的权限 |
PROCESS | All | 执行SHOW PROCESSLIST 命令的权限 |
RELOAD | All | 执行FLUSH等让数据库重新Load某些对象或者数据的命令的权限 |
REPLICATION CLIENT | 4.0.2+ | 执行SHOW MASTER STATUS 和 SHOW SLAVE STATUS命令的权限 |
REPLICATION SLAVE | 4.0.2+ | 复制环境中Slave连接用户所需要的复制权限 |
SELECT | All | 数据查询权限 |
SHOW DATABASES | 4.0.2+ | 执行 SHOW DATABASES 命令的权限 |
SHOW VIEW | 5.0.1+ | 执行 SHOW CREATE VIEW 命令查看view创建语句的权限 |
SHUTDOWN | All | MySQL Server的shut down 权限(如通过mysqladmin执行 shutdown命令所使用的连接用户) |
SUPER | 4.0.2+ | 执行kill 线程,CHANGE MASTER, PURGE MASTER LOGS, and SET GLOBAL等命令的权限 |
UPDATE | All | 更新数据的权限 |
USAGE | All | 新创建用户后不授任何权限的时候所拥有的最小权限 |
要授予Global Level的权限,则只需要在执行GRANT命令的时候,用“*.*”来指定适用范围是Global的即可,当有多个权限需要授予的时候,也并不需要多次重复执行GRANT命令,只需要一次将所有需要的权限名称通过逗号(“,”)分隔开即可,如下:
root@localhost : mysql 05:14:35> GRANT SELECT,UPDATE,DELETE,INSERT ON *.* TO 'def'@'localhost';
Query OK, 0 rows affected (0.00 sec)
2、Database Level
Database Level是在Global Level之下,其他三个Level之上的权限级别,其作用域即为所指定整个数据库中的所有对象。与Global Level的权限相比,Database Level主要少了以下几个权限:CREATE USER,FILE,PROCESS,RELOAD,REPLICATION CLIENT,REPLICATION SLAVE,SHOW DATABASES,SHUTDOWN,SUPER和USAGE这几个权限,没有增加任何权限。之前我们说过Global Level的权限会覆盖底下其他四层的相同权限,Database Level也一样,虽然他自己可能会被Global Level的权限设置所覆盖,但同时他也能覆盖比他更下层的Table,Column和Routine这三层的权限。
如果要授予Database Level的权限,则可以有两种实现方式:
1、在执行GRANT命令的时候,通过“database.*”来限定权限作用域为database整个数据库,如下:
root@localhost : mysql 06:06:26> GRANT ALTER ON test.* TO 'def'@'localhost';
Query OK, 0 rows affected (0.00 sec)
root@localhost : test 06:12:45> SHOW GRANTS FOR def@localhost;
+------------------------------------------------------------------+
| Grants for def@localhost |
+------------------------------------------------------------------+
| GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO 'def'@'localhost' |
| GRANT ALTER ON `test`.* TO 'def'@'localhost' |
+------------------------------------------------------------------+
2、先通过USE命令选定需要授权的数据库,然后通过“*”来限定作用域,这样授权的作用域实际上就是当前选定的整个数据库。
root@localhost : mysql 06:14:05> USE test;
Database changed
root@localhost : test 06:13:10> GRANT DROP ON * TO 'def'@'localhost';
Query OK, 0 rows affected (0.00 sec)
root@localhost : test 06:15:26> SHOW GRANTS FOR def@localhost;
+------------------------------------------------------------------+
| Grants for def@localhost |
+------------------------------------------------------------------+
| GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO 'def'@'localhost' |
| GRANT DROP, ALTER ON `test`.* TO 'def'@'localhost' |
+------------------------------------------------------------------+
在授予权限的时候,如果有相同的权限需要授予多个用户,我们也可以在授权语句中一次写上多个用户信息,通过逗号(,)分隔开就可以了,如下:
root@localhost : mysql 05:22:32> grant create on perf.* to 'abc'@'localhost','def'@'localhost';
Query OK, 0 rows affected (0.00 sec)
root@localhost : mysql 05:22:46> SHOW GRANTS FOR def@localhost;
+------------------------------------------------------------------+
| Grants for def@localhost |
+------------------------------------------------------------------+
| GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO 'def'@'localhost' |
| GRANT DROP, ALTER ON `test`.* TO 'def'@'localhost' |
| GRANT CREATE ON `perf`.* TO 'def'@'localhost' |
+------------------------------------------------------------------+
3 rows in set (0.00 sec)
root@localhost : mysql 05:23:13> SHOW GRANTS FOR abc@localhost;
+------------------------------------------------------------------+
| Grants for abc@localhost |
+------------------------------------------------------------------+
| GRANT CREATE ON `perf`.* TO 'abc'@'localhost' |
| GRANT SELECT ON `test`.* TO 'abc'@'localhost' |
+------------------------------------------------------------------+
3 rows in set (0.00 sec)
3、Table Level
Database Level之下就是Table Level的权限了,Table Level的权限可以被Global Level和Database Level的权限所覆盖,同时也能覆盖Column Level和Routine Level的权限。
Table Level的权限作用范围是授权语句中所指定数据库的指定表。如可以通过如下语句给test数据库的t1表授权:
root@localhost : test 12:02:15> GRANT INDEX ON test.t1 TO 'abc'@'%.jianzhaoyang.com';
Query OK, 0 rows affected, 1 warning (0.00 sec)
root@localhost : test 12:02:53> SHOW GRANTS FOR 'abc'@'%.jianzhaoyang.com';
+----------------------------------------------------------+
| Grants for abc@*.jianzhaoyang.com |
+----------------------------------------------------------+
| GRANT USAGE ON *.* TO 'abc'@'%.jianzhaoyang.com' |
| GRANT INDEX ON `test`.`t1` TO 'abc'@'%.jianzhaoyang.com' |
+----------------------------------------------------------+
上面的授权语句在测试给test数据库的t1表授予Table Level的权限的同时,还测试了将权限授予含有通配符“%”的所有“.jianzhaoyang.com”主机。其中的USAGE权限是每个用户都有的最基本权限。
Table Level的权限由于其作用域仅限于某个特定的表,所以权限种类也比较少,仅有ALTER,CREATE,DELETE,DROP,INDEX,INSERT,SELECT UPDATE这八种权限。
4、Column Level
Column Level的权限作用范围就更小了,仅仅是某个表的指定的某个(活某些)列。由于权限的覆盖原则,Column Level的权限同样可以被Global,Database,Table这三个级别的权限中的相同级别所覆盖,而且由于Column Level所针对的权限和Routine Level的权限作用域没有重合部分,所以不会有覆盖与被覆盖的关系。针对Column Level级别的权限仅有INSERT,SELECT和UPDATE这三种。Column Level的权限授权语句语法基本和Table Level差不多,只是需要在权限名称后面将需要授权的列名列表通过括号括起来,如下:
root@localhost : test 12:14:46> GRANT SELECT(id,value) ON test.t2 TO 'abc'@'%.jianzhaoyang.com';
Query OK, 0 rows affected(0.00 sec)
root@localhost : test 12:16:49> SHOW GRANTS FOR 'abc'@'%.jianzhaoyang.com';
+-----------------------------------------------------------------------+
| Grants for abc@*.jianzhaoyang.com |
+-----------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'abc'@'%.jianzhaoyang.com' |
| GRANT SELECT (value, id) ON `test`.`t2` TO 'abc'@'%.jianzhaoyang.com' |
| GRANT INDEX ON `test`.`t1` TO 'abc'@'%.jianzhaoyang.com' |
+-----------------------------------------------------------------------+
注意:当某个用户在向某个表插入(INSERT)数据的时候,如果该用户在该表中某列上面没有INSERT权限,则该列的数据将以默认值填充。这一点和很多其他的数据库都有一些区别,是MySQL自己在SQL上面所做的扩展。
5、Routine Level
Routine Level的权限主要只有EXECUTE和ALTER ROUTINE两种,主要针对的对象是procedure和function这两种对象,在授予Routine Level权限的时候,需要指定数据库和相关对象,如:
root@localhost : test 04:03:26> GRANT EXECUTE ON test.p1 to 'abc'@'localhost';
Query OK, 0 rows affected (0.00 sec)
除了上面几类权限之外,还有一个非常特殊的权限GRANT,拥有GRANT权限的用户可以将自身所拥有的任何权限全部授予其他任何用户,所以GRANT权限是一个非常特殊也非常重要的权限。GRANT权限的授予方式也和其他任何权限都不太一样,通常都是通过在执行GRANT授权语句的时候在最后添加WITH GRANT OPTION子句达到授予GRANT权限的目的。
此外,我们还可以通过GRANT ALL 语句授予某个Level的所有可用权限给某个用户,如:
root@localhost : test 04:15:48> grant all on test.t5 to 'abc';
Query OK, 0 rows affected (0.00 sec)
root@localhost : test 04:27:39> grant all on perf.* to 'abc';
Query OK, 0 rows affected (0.00 sec)
root@localhost : test 04:27:52> show grants for 'abc';
+--------------------------------------------------+
| Grants for abc@% |
+--------------------------------------------------+
| GRANT USAGE ON *.* TO 'abc'@'%' |
| GRANT ALL PRIVILEGES ON `perf`.* TO 'abc'@'%' |
| GRANT ALL PRIVILEGES ON `test`.`t5` TO 'abc'@'%' |
+--------------------------------------------------+
在以上五个Level的权限中,Table、Column和Routine三者在授权中所依赖(或者引用)的对象必须是已经存在的,而不像Database Level的权限授予,可以在当前不存在该数据库的时候就完成授权。
4.2.4 MySQL访问控制实现原理
MySQL访问控制实际上由两个功能模块共同组成,从第一篇的第二章架构组成中可以看到,一个是负责“看守MySQL大门”的用户管理模块,另一个就是负责监控来访者每一个动作的访问控制模块。用户管理模块决定造访客人能否进门,而访问控制模块则决定每个客人进门能拿什么不能拿什么。下面是一张MySQL中实现访问控制的简单流程图(见图4-2):
图4-2
1、 用户管理
我们先看看用户管理模块是如何工作的。在MySQL中,用户访问控制部分的实现比较简单,所有授权用户都存放在一个系统表中:mysql.user,当然这个表不仅仅存放了授权用户的基本信息,还存放有部分细化的权限信息。用户管理模块需要使用的信息很少,主要就是Host,User,Password这三项,都在mysql.user表中,如下:
sky@localhost : (none) 12:35:04> USE mysql;
Database changed
sky@localhost : mysql 12:35:08> DESC user;
+---------------+--------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------------+------+-----+---------+-------+
| Host | char(60) | NO | PRI | | |
| User | char(16) | NO | PRI | | |
| Password | char(41) | NO | | | |
... ...
+---------------+--------------------+------+-----+---------+-------+
一个用户要想访问MySQL,至少需要提供上面列出的这三项数据,MySQL才能判断是否该让他“进门”。这三项实际上由量部分组成:访问者来源的主机名(或者主机IP地址信息)和访问者的来访“暗号”(登录用户名和登录密码),这两部分中的任何一个没有能够匹配上都无法让看守大门的用户管理模块乖乖开门。其中Host信息存放的是MySQL允许所对应的User的信任主机,可以是某个具体的主机名(如:mytest)或域名(如:www.domain.com),也可以是以“%”来充当通配符的某个域名集合(如:%.domain.com);也可以是一个具体的IP地址(如:1.2.3.4),同样也可以是存在通配符的域名集合(如:1.2.3.%);还可以用“%”来代表任何主机,就是不对访问者的主机做任何限制。如以下设置:
root@localhost : mysql 01:18:12> SELECT host,user,password FROM user ORDER BY user;
+--------------------+------+-------------------------------------------+
| host | user | password |
+--------------------+------+-------------------------------------------+
| % | abc | |
| *.jianzhaoyang.com | abc | |
| localhost | abc | *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 |
| 1.2.3.4 | abc | *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 |
| 1.2.3.* | def | *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 |
| % | def | *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 |
| localhost | def | *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 |
... ...
+--------------------+------+-------------------------------------------+
但是这里有一个比较特殊的访问限制,如果要通过localhost访问的话,必须要有一条专门针对localhost的授权信息,即使不对任何主机做限制也不行。如下例所示,存在def@%的用户设置,但是如果不使用-h参数来访问,则登录会被拒绝,因为mysql在默认情况下会连接localhost:
sky@sky:~$ mysql -u def -p
Enter password:
ERROR 1045 (28000): Access denied for user 'def'@'localhost' (using password: YES)
但是当通过-h参数,明确指定了访问的主机地址之后就没问题了,如下:
sky@sky:~$ mysql -u def -p -h 127.0.0.1
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 17
Server version: 5.0.51a-log Source distribution
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
def@127.0.0.1 : (none) 01:26:04>
如果我们有一条localhost的访问授权则可以不使用-h参数来指定登录host而连接默认的localhost:
sky@sky:~$ mysql -u abc -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 18
Server version: 5.0.51a-log Source distribution
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
abc@localhost : (none) 01:27:19> exit
Bye
如果MySQL正在运行之中的时候,我们对系统做了权限调整,那调整之后的权限什么时候会生效呢?
我们先了解何时MySQL存放于内存结构中的权限信息被更新:FLUSH PRIVILEGES会强行让MySQL更新Load到内存中的权限信息;GRANT、REVOKE或者CREATE USER和DROP USER操作会直接更新内存中俄权限信息;重启MySQL会让MySQL完全从grant tables中读取权限信息。
那内存结构中的权限信息更新之后对已经连接上的用户何时生效呢?
对于Global Level的权限信息的修改,仅仅只有更改之后新建连接才会用到,对于已经连接上的session并不会受到影响。而对于Database Level的权限信息的修改,只有当客户端请求执行了“USE database_name”命令之后,才会在重新校验中使用到新的权限信息。所以有些时候如果在做了比较紧急的Global和Database这两个Level的权限变更之后,可能需要通过“KILL”命令将已经连接在MySQL中的session 杀掉强迫他们重新连接以使用更新后的权限。对于Table Level和Column Level的权限,则会在下一次需要使用到该权限的Query被请求的时候生效,也就是说,对于应用来讲,这两个Level的权限,更新之后立刻就生效了,而不会需要执行“KILL”命令。
2、 访问控制
当客户端连接通过用户管理模块的验证,可连接上MySQL Server之后,就会发送各种Query和Command给MySQL Server,以实现客户端应用的各种功能。当MySQL 接收到客户端的请求之后,访问控制模块是需要校验该用户是否满足提交的请求所需要的权限。权限校验过程是从最大范围的权限往最小范围的权限开始依次校验所涉及到的每个对象的每个权限。
在验证所有所需权限的时候,MySQL首先会查找存储在内存结构中的权限数据,首先查找 Global Level 权限,如果所需权限在 Global Level 都有定义(GRANT或者REVOKE),则完成权限校验(通过或者拒绝),如果没有找到所有权限的定义,则会继续往后查找 Database Level 权限,进行 Global Level 未定义的所需权限的校验,如果仍然没有能够找到所有所需权限的定义,MySQL会继续往更小范围的权限定义域查找,也就是 Table Level,最后则是 Column Level 或者 Routine Level。
下面我们就以客户端通过abc@localhost连接后请求如下Query我为例:
SELECT id,name FROM test.t4 where status = 'deleted';
图4-3
在前面我们了解到MySQL的grant tables有mysql.user,mysql.db,mysql.host,mysql.table_priv和mysql.column_priv这五个,我想出了mysql.host之外的四个都是非常容易理解的,每一个表针对MySQL中的一种逻辑对象,存放某一特定Level的权限,唯独mysql.host稍有区别。我们现在就来看看mysql.host权限表到底在MySQL的访问控制中充当了一个什么样的角色呢?
mysql.host在MySQL访问控制模块中所实现的功能比较特殊,和其他几个grant tables不太一样。首先是mysql.host中的权限数据不是(也不能)通过GRANT或者REVOKE来授予或者去除,必须通过手工通过INSERT、UPDATE和DELETE 命令来修改其中的数据。其次是其中的权限数据无法单独生效,必须通过和mysql.db权限表的数据一起才能生效。而且仅当mysql.db中存在不完整(某些场景下的特殊设置)的时候,才会促使访问控制模块再结合mysql.host中查找是否有相应的补充权限数据实现以达到权限校验的目的,就比如上图中所示。在mysql.db中无法找到满足权限校验的所有条件的数据(db.User = 'abc' AND db.host = 'localhost' AND db.Database_name = 'test'),则说明在mysql.db中无法完成权限校验,所以也不会直接就校验db.Select_priv的值是否为'Y'。但是mysql.db中有db.User = 'abc' AND db.Database_name = 'test' AND db.host = '' 这样一条权限信息存在,大家可能注意到了这条权限信息中的db.host中是空值,注意是空值而不是'%'这个通配符哦。当MySQL注意到有这样一条权限信息存在的时候,就该是mysql.host中所存放的权限信息出场的时候了。这时候,MySQL会检测mysql.host中是否存在满足如下条件的权限信息:host.Host = 'localhost' AND host.Db = 'test'。如果存在,则开始进行Select_priv权限的校验。由于权限信息存在于mysql.db和mysql.host两者之中,而且是两者信息合并才能满足要求,所以Select_priv的校验也需要两表都为'Y'才能满足要求,通过校验。
我们已经清楚,MySQL的权限是授予“username@hostname”的,也就是说,至少需要用户名和主机名二者才能确定一个访问者的权限。又由于hostname可以是一个含有通配符的域名,也可以是一个含有通配符的IP地址段。那么如果同一个用户有两条权限信息,一条是针对特定域名的,另外一个是含有通配符的域名,而且前者属于后者包含。这时候MySQL如何来确定权限信息呢?实际上MySQL永远优先考虑更精确范围的权限。在MySQL内部会按照username和hostname作一个排序,对于相同username的权限,其host信息越接近访问者的来源host,则排序位置越靠前,则越早被校验使用到。而且,MySQL在权限校验过程中,只要找到匹配的权限之后,就不会再继续往后查找是否还有匹配的权限信息,而直接完成校验过程。
大家应该也看到了在mysql.user这个权限表中有max_questions,max_updates,max_connections,max_user_connections这四列,前面三列是从MySQL4.0.2版本才开始有的,其功能是对访问用户进行每小时所使用资源的限制,而最后的max_user_connections则是从MySQL5.0.3版本才开始有的,他和max_connections的区别是限制耽搁用户的连接总次数,而不是每小时的连接次数。而要使这四项限制生效,需要在创建用户或者给用户授权的时候加上以下四种子句:
max_questions : WITH MAX_QUERIES_PER_HOUR n;
max_updates : WITH MAX_UPDATES_PER_HOUR n;
max_connections : WITH MAX_CONNECTIONS_PER_HOUR n;
max_user_connections: MAX_USER_CONNECTIONS。
四个子句可以同时使用,如:
“WITH MAX_QUERIES_PER_HOUR 5000 MAX_CONNECTIONS_PER_HOUR 10 MAX_USER_CONNECTIONS 10000”。
4.3 MySQL 访问授权策略
在我们了解了影响数据库系统安全的相关因素以及MySQL权限系统的工作原理之后,就需要为我们的系统设计一个安全合理的授权策略。我想,每个人心里都清楚,要想授权最简单最简单方便,维护工作量最少,那自然是将所有权限都授予所有的用户来的最简单方便了。但是,我们大家肯定也都知道,一个用户所用有的权限越大,那么他给我们的系统所带来的潜在威胁也就越大。所以,从安全方面来考虑的话,权限自然是授予的越小越好。一个有足够安全意识的管理员在授权的时候,都会只授予必要的权限,而不会授予任何多余的权限。既然我们这一章是专门讨论安全的,那么我们现在也就从安全的角度来考虑如何设计一个更为安全合理的授权策略。
首先,需要了解来访主机。
由于MySQL数据库登录验证用户的时候是出了用户名和密码之外,还要验证来源主机。所以我们还需要了解每个用户可能从哪些主机发起连接。当然,我们也可以通过授权的时候直接通过“%”通配符来给所有主机都有访问的权限,但是这样作就违背了我们安全策略的原则,带来了潜在风险,所以并不可取。尤其是在没有局域网的防火墙保护的情况下,更是不能轻易允许可以从任何主机登录的用户存在。能通过具体主机名或者IP地址指定的尽量通过使用具体的主机名和IP地址来限定来访主机,不能用具体的主机名或者IP地址限定的也需要用尽可能小的通配范围来限定。
其次,了解用户需求。
既然是要做到仅授予必要的权限,那么我们必须了解每个用户所担当的角色,也就是说,我们需要充分了解每个用户需要连接到数据库上完成什么工作。了解该用户是一个只读应用的用户,还是一个读写都有的帐户;是一个备份作业的用户还是一个日常管理的帐户;是只需要访问特定的某个(或者某几个)数据库(Schema),还是需要访问所有的数据库。只有了解了需要做什么,才能准确的了解需要授予什么样的权限。因为如果权限过低,会造成工作无法正常完成,而权限过高,则存在潜在的安全风险。
再次,要为工作分类。
为了做到各司其职,我们需要将需要做的工作分门别类,不同类别的工作使用不同的用户,做好用户分离。虽然这样可能会带来管理成本方面的部分工作量增加,但是基于安全方面的考虑,这部分管理工作量的增加是非常值得的。而且我们所需要做的用户分离也只是一个适度的分离。比如将执行备份工作、复制工作、常规应用访问、只读应用访问和日常管理工作分别分理出单独的特定帐户来授予各自所需权限。这样,既可以让安全风险尽量降低,也可以让同类同级别的相似权限合并在一起,不互相交织在一起。对于PROCESS,FILE和SUPER这样的特殊权限,仅仅只有管理类帐号才需要,不应该授予其他非管理帐号。
最后,确保只有绝对必要者拥有GRANT OPTION 权限。
之前在权限系统介绍的时候我们已经了解到GRANT OPTION权限的特殊性,和拥有该权限之后的潜在风险,所以在这里也就不再累述了。总之,为了安全考虑,拥有GRANT OPTION权限的用户越少越好,尽可能只让拥有超级权限的用户才拥有GRANT OPTION权限。
4.4 安全设置注意事项
在前面我们了解了影响数据库系统安全的几个因素,也了解了MySQL权限系统的相关原理和实现,这一节我们将针对这些因素进行一些基本的安全设置讨论,了解一些必要的注意事项。
首先,自然是最外围第一层防线的网络方面的安全。
我们首先要确定我们所维护的MySQL环境是否真的需要提供网络服务?是否可以使我们的MySQL仅仅提供本地访问,而禁止网络服务?如果可以,那么我们可以在启动MySQL的时候通过使用“--skip-networking”参数选项,让MySQL不通过TCP/IP监听网络请求,而仅仅通过命名管道或共享内存(在Windows中)或Unix套接字文件(在Unix中)来和客户端连接交互。
当然,在本章最开始的时候,我们就已经讨论过,由于MySQL数据库在大部分应用场景中都是在网络环境下,通过网络连接提供服务。所以我们只有少部分应用能通过禁用网络监听来断绝网络访问以保持安全,剩下的大部分还是需要通过其他方案来解决网络方面存在的潜在安全威胁。
使用私有局域网络。我们可以通过使用私有局域网络,通过网络设备,统一私有局域网的出口,并通过网络防火墙设备控制出口的安全。
使用SSL加密通道。如果我们的数据对保密要求非常严格,可以启用MySQL提供的SSL访问接口,将传输数据进行加密。使网络传输的数据即使被截获,也无法轻易使用。
访问授权限定来访主机信息。在之前的权限系统介绍中我们已经了解到MySQL的权限信息是针对用户和来访主机二者结合定位的。所以我们可以在授权的时候,通过指定主机的主机名、域名或者IP地址信息来限定来访主机的范围。
其次,在第二层防线主机上面也有以下一些需要注意的地方。
OS安全方面。关闭MySQL Server主机上面任何不需要的服务,这不仅能从安全方面减少潜在隐患,还能减轻主机的部分负担,尽可能提高性能。使用网络扫描工具(如nmap等)扫描主机端口,检查除了MySQL需要监听的端口3306(或者自定义更改后的某个端口)之外,还有哪些端口是打开正在监听的,并去掉不必要的端口。严格控制OS帐号的管理,以防止帐号信息外泄,尤其是root和mysql帐号。对root和mysql等对mysql的相关文件有特殊操作权限的OS帐号登录后做出比较显眼的提示,并在Terminal的提示信息中输出当前用户信息,以防止操作的时候经过多次用户切换后出现人为误操作。
用非root用户运行MySQL。这在MySQL官方文档中也有非常明显的提示,提醒用户不要使用root用户来运行MySQL。因为如果使用root用户运行MySQL,那么mysqld的进程就会拥有root用户所拥有的权限,任何具有FILE权限的MySQL用户就可以在MySQL中向系统中的任何位置写入文件。当然,由于MySQL不接受操作系统层面的认证,所以任何操作系统层级的帐号都不能直接登录MySQL,这一点和Oracle的权限认证有些区别,所以在这一方面我们可以减少一些安全方面的顾虑。
文件和进程安全。合理设置文件的权限属性,MySQL相关的数据和日志文件和所在的文件夹属主和所属组都设置为mysql,且禁用其他所有用户(除了拥有超级权限的用户,如root)的读写权限。以防止数据或者日志文件被窃取或破坏。因为如果一个用户对MySQL的数据文件有读取权限的话,可以很容易将数据复制。binlog文件也很容易还原整个数据库。而如果有写权限的话就更糟了,因为有了写权限,数据或者日志文件就有被破坏或者删除的风险存在。保护好socket文件的安全,尽量不要使用默认的位置(如/tmp/mysql.sock),以防止被有意或无意的删除。
确保MySQL Server所在的主机上所必要运行的其他应用或者服务足够安全,避免因为其他应用或者服务存在安全漏洞而被入侵者攻破防线。
在OS层面还有很多关于安全方面的其他设置和需要注意的地方,但考虑到篇幅问题,这里就不做进一步分析了,有兴趣的读者可以参考各种不同OS在安全方面的专业书籍。
再次,就是最后第三道防线MySQL自身方面的安全设置注意事项。
到了最后这道防线上,我们有更多需要注意的地方。
用户设置。我们必须确保任何可以访问数据库的用户都有一个比较复杂的内容作为密码,而不是非常简单或者比较有规律的字符,以防止被使用字典破解程序攻破。在MySQL初始安装完成之后,系统中可能存在一个不需要任何密码的root用户,有些版本安装完成之后还会存在一个可以通过localhost登录的没有用户名和密码的帐号。这些帐号会给系统带来极大的安全隐患,所以我们必须在正式启用之前尽早删除,或者设置一个比较安全的密码。对于密码数据的存放,也不要存放在简单的文本文件之中,而应该使用专业密码管理软件来管理(如KeePass)。同时,就像之前在网络安全注意事项部分讲到的那样,尽可能为每一个帐户限定一定范围的可访问主机。尤其是拥有超级权限的MySQL root帐号,尽量确保只能通过localhost访问。
安全参数。在MySQL官方参考手册中也有说明,不论是从安全方面考虑还是从性能以及功能稳定性方面考虑,不需要使用的功能模块尽量都不要启用。例如,如果不需要使用用户自定义函数,就不要在启动的时候使用“--allow-suspicious-udfs”参数选项,以防止被别有居心的潜在威胁者利用此功能而对MySQL的安全造成威胁;不需要从本地文件中Load数据到数据库中,就使用“--local-infile=0”禁用掉可以从客户端机器上Load文件到数据库中;使用新的密码规则和校验规则(不要使用“--old-passwords”启动数据库),这项功能是为了兼容旧版本的密码校验方式的,如无额数必要,不要使用该功能,旧版本的密码加密方式要比新的方式在安全方面弱很多。
除了以上这三道防线,我们还应该让连接MySQL数据库的应用程序足够安全,以防止入侵者通过应用程序中的漏洞而入侵到应用服务器,最终通过应用程序中的数据库相关关配置而获取数据库的登录口令。
4.5 小结
安全无小事,一旦安全出了问题一切都完了。数据的安全是一个企业安全方面最核心最重要的内容,只有保障的数据的安全,企业才有可能真正“安全”。希望这一章 MySQL 安全方面的内容能够对各位读者在构筑安全的企业级 MySQL 数据库系统中带来一点帮助。