Pull to refresh

Аудит и внешняя аутентификация в MySQL

Reading time11 min
Views6.9K
Сегодня я расскажу как сделать вашу СУБД MySQL ближе к стандартам PCI DSS. Для начала вот что у нас получится:
Консоль админ пользователя mcshadow
mcshadow:~$mysql --user=mcshadow --password=mike
mysql> select current_user();
+----------------+
| current_user() |
+----------------+
| mike@localhost |
+----------------+
mcshadow:~$mysql --user=mcshadow --password=root
mysql> select current_user();
+----------------+
| current_user() |
+----------------+
| root@localhost |
+----------------+

Доступ возможен как с правами рута, так и с правами смертного пользователя mike.


Консоль смертного пользователя mike
mike:~$mysql --user=mcshadow --password=mike
ERROR 1698 (28000): Access denied for user 'mcshadow'@'localhost'

Доступ к БД под администратором невозможен.


А тем временем в syslog
mysqld: User:mcshadow TRY access from:localhost with privileges:mike
mysqld: User:mcshadow SUCCESS access from:localhost with privileges:mike
mysql: SYSTEM_USER:'mcshadow', MYSQL_USER:'mcshadow', CONNECTION_ID:5, DB_SERVER:'--', DB:'--', COMMAND_RESULT:SUCCESS, QUERY:'select current_user();'
mysqld: User:mcshadow TRY access from:localhost with privileges:root
mysqld: User:mcshadow SUCCESS access from:localhost with privileges:root
mysql: SYSTEM_USER:'mcshadow', MYSQL_USER:'mcshadow', CONNECTION_ID:6, DB_SERVER:'--', DB:'--', COMMAND_RESULT:SUCCESS, QUERY:'select current_user();'
mysqld: User:mcshadow TRY access from:localhost with privileges:mike
mysqld: User:mcshadow FAILED access from:localhost with privileges:mike


Введение


Пост рекоммендуется тем, кому это действительно нужно, тут была фраза «сегодня буду краток», но по окончании написания статьи понял что не вышло.
Если вы, используете MySQL в качестве СУБД в крупной компании, вы столкнетесь с некоторыми проблемами, как то: в MySQL нет определяемой пользователем политики паролей, т.е. вы не сможете задать хитрую схему по экспирейшену используемого пароля, контролировать новые пароли на соответствие стандартам принятым в вашей организации, использовать систему SSO для коннекта к БД и т.д. Так же было бы крайне удобно логировать все успешные и неуспешные попытки соединения с БД и действия пользователей с правами DBA на пульт security офицера. По мимо этого часто очень хочется зайти в БД под своим логином, но с правами другого пользователя, к примеру для проведения установки или выполнения определенного набора действия для получения ошибки. При чем желательно не знать пароля этого пользователя, а так же сделать так, чтобы все действия корректно были отражены в логах безопасности. Другие БД позволяют сделать часть из этих пунктов, конечно далеко не все. MySQL начиная с версии 5.5.7+ позволит вам выполнить любой их них, без создания излишней нагрузки на БД.
Статья носит обучающий характер, использование данных решений в любых целях, включая описанные, исключительно на вашей совести.

Теория


Теперь поговорим что и откуда брать. Как вы понимаете, для того чтобы это заработало надо собрать внешнюю библиотеку. Ну мы как “опытные” сишники ничего сами писать не будем а просто возьмем готовое, и просто перекинем из одних исходников в другие.
Первое: логирование для клиента MySQL берем из перконы. Если сравнивать исходники MySQL 5.5.X и Percona 5.5.X, то различие в них исключительно в том, что перконовский клиент умеет логировать все подряд в syslog, но делает это опционально. Собственно нам надо просто перетащить часть этих исходных кодов. И сделать эту настройку по умолчанию. Можете просто взять исходник MySQL клиента для перконы, если боитесь накосячить при копипасте.
Второе: логироваение попыток входа в БД, как вы понимаете придется делать на сервере. Тут у нас выбор невелик. Как понять что надо логировать? Все просто в исходниках MySQL есть файл log.cc — он отвечает за general_log. Этот лог успешно записывает всё что происходит на БД, в том числе и успешный и неуспешные попытки коннекта. Все бы хорошо, но работает это из рук вон медленно — крайне не советую его включать на промышленной БД. Нам этот лог нужен для понимая того что искать и где. Согласно этому файлу на данный момент существует всего две имплементации позволяющие производить запись в лог general_log_print и general_log_write. Тут придется попотеть и внимательно просмотреть что менять и где.
Третье: и для нас пожалуй самое интересное — новая фича MySQL 5.5.7 GRANT PROXY.
GRANT PROXY
  ON 'priv_user'@'localhost'
  TO 'real_user'@'localhost';

чтобы это заработало пользователь real_user должен быть создан особым образом
CREATE USER 'real_user'@'localhost'
  IDENTIFIED WITH 'auth_plugin_xxx' AS 'auth_string';

Теперь при коннекте пользователя real_user пароль можно проверять не просто средствами самого мускуля, а возложить это на сторонний плагин — auth_plugin_xxx. Можно написать этот плагин самостоятельно тынц, ну это для тру девелоперов, мы же такими вещами, пока, заниматься не будем, потому что: в целях тестирования и в качестве примера MySQL уже написала парочку плагинов, на которых можно ставить эксперименты. Их мы и возьмем за основу чтобы поиграться. Самое главное, для чего нужен этот плагин это то, что он может подменить на основании своей внутренней логики поле имя пользователя привилегии которого будет применены к сеансу. И если наш real_user имеет права на proxy под подмененным пользователем — MySQL успешно предоставит нам все права пользователя priv_user. Именно в этот плагим можно впихнуть обращение к SSO по вашему внутреннем у протоколу или же к ldap серверу, и наворотить всякую кучу другой логики.
Пока хватит теории — качаем MySQL 5.5.15 в исходных кодах.

Практика


Первое — логирование действий администратора с локальной машины. Считаем, что администратор БД не является администратором сервера и имеет доступ к БД только с консоли с использованием сокета либо по TCP — это не принципиально. Для того чтобы администрировать сервер этого более чем достаточно. В mysql.cc нам необходимо добавить следущие строчки:
#include <violite.h> // после этой строки для Linux систем включаем syslog
#ifndef __WIN__
#include "syslog.h"
#endif
...
void tee_putc(int c, FILE *file); // после этой декларируем внутреннюю функцию логирования
void write_syslog(String *buffer);
...
// в конец файла добавляем имплементацию из Percona слегка её изменив чтобы логировать успешность выполения комманды. Стек ошибок мускуль хранит тут: mysql_error(&mysql)[0]
void write_syslog(String *line){
#ifndef __WIN__
  uint length= line->length();
  uint chunk_len= min(MAX_SYSLOG_MESSAGE, length);
  char *ptr= line->c_ptr_safe();
  char buff[MAX_SYSLOG_MESSAGE + 1];

  for (;
       length;
       length-= chunk_len, ptr+= chunk_len, chunk_len= min(MAX_SYSLOG_MESSAGE,
                                                           length))
  {
    char *str;
    if (length == chunk_len)
      str= ptr;                                 // last chunk => skip copy
    else
    {
      memcpy(buff, ptr, chunk_len);
      buff[chunk_len]= '\0';
      str= buff;
    }
    syslog(LOG_INFO,
           "SYSTEM_USER:'%s', MYSQL_USER:'%s', CONNECTION_ID:%lu, "
           "DB_SERVER:'%s', DB:'%s', COMMAND_RESULT:%s, QUERY:'%s'",
           getenv("SUDO_USER") ? getenv("SUDO_USER") : 
           getenv("USER") ? getenv("USER") : "--",
           current_user ? current_user : "--",
           mysql_thread_id(&mysql),
           current_host ? current_host : "--",
           current_db ? current_db : "--",
           mysql_error(&mysql)[0]?"FAILED":"SUCCESS",
		   str);
  }
#endif
}
...
#endif /*HAVE_READLINE*/  // логирование сомманды выполняем после этой строки 

#ifndef __WIN__
  if (buffer->length() && connect_flag == CLIENT_INTERACTIVE){
    write_syslog(buffer);
  }
#endif

Второе. Главный модуль, который обрабатывает попытки логина пользователя на стороне сервера — sql_acl.cc. После вызова всех general_log_print — нам необходимо добавить свои. Как вы понимаете general_log у нас отключен, но наводка очень хороша. Вторая комманда (general_log_write) на данный момент не вызывается при попытках присоединения пользователя к БД. У меня получилось так (новые блоки выделены PCI DSS patch):
// PCI DSS patch
#ifndef __WIN__
#include "syslog.h"
#endif
// end PCI DSS patch
... // функция login_failed_error - вызывается если аутентификция не прошла
    general_log_print(thd, COM_CONNECT, ER(ER_ACCESS_DENIED_NO_PASSWORD_ERROR),
                      mpvio->auth_info.user_name,
                      mpvio->auth_info.host_or_ip);
// PCI DSS patch
	syslog(LOG_WARNING, "User:%s FAILED access from:%s with privileges:%s", mpvio->auth_info.user_name, mpvio->auth_info.host_or_ip, mpvio->auth_info.authenticated_as);
// end PCI DSS patch
...
    general_log_print(thd, COM_CONNECT, ER(ER_ACCESS_DENIED_ERROR),
                      mpvio->auth_info.user_name,
                      mpvio->auth_info.host_or_ip,
                      passwd_used ? ER(ER_YES) : ER(ER_NO));
// PCI DSS patch
	syslog(LOG_WARNING, "User:%s FAILED access from:%s with privileges:%s", mpvio->auth_info.user_name, mpvio->auth_info.host_or_ip, mpvio->auth_info.authenticated_as);
// end PCI DSS patch
... // функция secure_auth - это проверка на свежесть протокола испольщуемого клиентом
  if (mpvio->client_capabilities & CLIENT_PROTOCOL_41)
  {
    my_error(ER_SERVER_IS_IN_SECURE_AUTH_MODE, MYF(0),
             mpvio->auth_info.user_name,
             mpvio->auth_info.host_or_ip);
    general_log_print(thd, COM_CONNECT, ER(ER_SERVER_IS_IN_SECURE_AUTH_MODE),
                      mpvio->auth_info.user_name,
                      mpvio->auth_info.host_or_ip);
// PCI DSS patch
	syslog(LOG_WARNING, "User:%s FAILED access from:%s with privileges:%s", mpvio->auth_info.user_name, mpvio->auth_info.host_or_ip, mpvio->auth_info.authenticated_as);
// end PCI DSS patch
  }
  else
  {
    my_error(ER_NOT_SUPPORTED_AUTH_MODE, MYF(0));
    general_log_print(thd, COM_CONNECT, ER(ER_NOT_SUPPORTED_AUTH_MODE));
// PCI DSS patch
	syslog(LOG_WARNING, "Auth mode not supported");
// end PCI DSS patch
  }
... // функция send_plugin_request_packet - эта функция заставляет клиента ввести дополнительные данные если аутентификационный плагин этого требует
    general_log_print(current_thd, COM_CONNECT, ER(ER_NOT_SUPPORTED_AUTH_MODE));
// PCI DSS patch
	syslog(LOG_WARNING, "Auth mode not supported");
// end PCI DSS patch
... // find_mpvio_user
    general_log_print(current_thd, COM_CONNECT, ER(ER_NOT_SUPPORTED_AUTH_MODE));
// PCI DSS patch
	syslog(LOG_WARNING, "Auth mode not supported");
// end PCI DSS patch
... // функция acl_authenticate - ну это само ядро аутентификации main так сказать
    if (strcmp(mpvio.auth_info.authenticated_as, mpvio.auth_info.user_name))
    {
      general_log_print(thd, command, "%s@%s as %s on %s",
                        mpvio.auth_info.user_name, mpvio.auth_info.host_or_ip,
                        mpvio.auth_info.authenticated_as ? 
                          mpvio.auth_info.authenticated_as : "anonymous",
                        mpvio.db.str ? mpvio.db.str : (char*) "");
// PCI DSS patch
	syslog(LOG_WARNING, "User:%s TRY access from:%s with privileges:%s", mpvio.auth_info.user_name, mpvio.auth_info.host_or_ip, mpvio.auth_info.authenticated_as);
// end PCI DSS patch
    }
    else
      {
	  general_log_print(thd, command, (char*) "%s@%s on %s",
                        mpvio.auth_info.user_name, mpvio.auth_info.host_or_ip,
                        mpvio.db.str ? mpvio.db.str : (char*) "");
// PCI DSS patch
	syslog(LOG_WARNING, "User:%s TRY access from:%s with privileges:%s", mpvio.auth_info.user_name, mpvio.auth_info.host_or_ip, mpvio.auth_info.authenticated_as);
// end PCI DSS patch
      }
...
  if (res > CR_OK && mpvio.status != MPVIO_EXT::SUCCESS)
  {
    DBUG_ASSERT(mpvio.status == MPVIO_EXT::FAILURE);

    if (!thd->is_error())
      login_failed_error(&mpvio, mpvio.auth_info.password_used);
    DBUG_RETURN (1);
  }
// PCI DSS patch
  else
	syslog(LOG_WARNING, "User:%s SUCCESS access from:%s with privileges:%s", mpvio.auth_info.user_name, mpvio.auth_info.host_or_ip, mpvio.auth_info.authenticated_as);
// end PCI DSS patch

К сожалению это ещё не все. Оказывается выполнение аутентификации производится ещё в 2-х местах. Первое — при попытке выполнить комманду use database. За это отвечает модуль sql_db.cc функция mysql_change_db в неё после вызова генерального лога, как удачно, добавим наши строчки.
    general_log_print(thd, COM_INIT_DB, ER(ER_DBACCESS_DENIED_ERROR),
                      sctx->priv_user, sctx->priv_host, new_db_file_name.str);
// PCI DSS patch
	syslog(LOG_WARNING, "User:%s FAILED access from:%s with privileges:%s", sctx->proxy_user, sctx->priv_host, sctx->priv_user);
// end PCI DSS patch

И последнее что нам осталось сделать это логировать момент просмотра пользователем информации по БД которая ему недоступна. За это отвечает модуль sql_show.cc. Процедура с красноречивым названием mysqld_show_create_db. Добавляем:
    general_log_print(thd,COM_INIT_DB,ER(ER_DBACCESS_DENIED_ERROR),
                      sctx->priv_user, sctx->host_or_ip, dbname);
// PCI DSS patch
	syslog(LOG_WARNING, "User:%s FAILED access from:%s with privileges:%s", sctx->proxy_user, sctx->priv_host, sctx->priv_user);
// end PCI DSS patch

Согласно POSIX функция syslog поддерживает многопоточность, падать не должно. В критические секции мы не влезли, сильно тормозть не должно.
Отлично! Теперь мы сможем читать в логах как и где у нас меняются права. Осталось дело за малым. Заюзать новую фичу. Лезем в каталог plugins исходных кодов и видим целых два плагина на аутентификацию, вот так подарок. Первый auth_socket.c — позволяет логинится в БД под пользователем операционной системы, если используется сокет. Что ж за неимением лучшего воспользуемся им — типа это наш SSO. Следующий плагин — test_plugin.c — работает следующим образом. При создании пользователя вы указываете мистическую строчку AS 'auth_string' после имени плагина. Плагин сравнивает пароль именно с этой строчкой. Если совпадений не выявлено, он вываливает ошибку, если же все прошло хорошо — то вам на сеанс назначаются привелегии именно пользователя, чье имя и есть 'auth_string'. Плагин, как вы сам понимаете тестовый и служит просто для цели проверки что механизм работает.
Согласно документации плагин может менять только имя пользователя, вернее писать его с новое специально отведенное для этого поле info->authenticated_as и задавать необходимость ввода пароля
#define PASSWORD_USED_NO         0
#define PASSWORD_USED_YES        1
#define PASSWORD_USED_NO_MENTION 2

Со сменой хоста у меня возникли проблемы, так что рисковать не стал. Все только с локалки.
Делаем из двух функций одну и пихаем её в тестовый плагин аутентификации (вдруг другой системой юзается ...)
static int auth_test_plugin(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info)
{
  unsigned char *pkt;
  int pkt_len;
  MYSQL_PLUGIN_VIO_INFO vio_info;
  struct ucred cred;
  socklen_t cred_len= sizeof(cred);
  struct passwd pwd_buf, *pwd;
  char buf[1024];

  /* запрашиваем пароль */
  if (vio->write_packet(vio, (const unsigned char *) PASSWORD_QUESTION, 1))
    return CR_ERROR;

  /* считываем пароль */
  if ((pkt_len= vio->read_packet(vio, &pkt)) < 0)
    return CR_ERROR;

  /* указываем что на пароль можно забить */
  info->password_used= PASSWORD_USED_NO_MENTION;

  /* копируем полученный пароль в качестве имени пользователя */
  strcpy (info->authenticated_as, (const char *) pkt);

  vio->info(vio, &vio_info);
  if (vio_info.protocol != MYSQL_VIO_SOCKET)
    return CR_ERROR;

  /* get the UID of the client process */
  if (getsockopt(vio_info.socket, SOL_SOCKET, SO_PEERCRED, &cred, &cred_len))
    return CR_ERROR;

  if (cred_len != sizeof(cred))
    return CR_ERROR;

  /* and find the username for this uid */
  getpwuid_r(cred.uid, &pwd_buf, buf, sizeof(buf), &pwd);
  if (pwd == NULL)
    return CR_ERROR;

  /* проверяем что пользователь ОС совпадает с пользователем MySQL */
  return strcmp(pwd->pw_name, info->user_name) ? CR_ERROR : CR_OK;
}

Далее собираемся
cmake -DCMAKE_INSTALL_PREFIX=/opt/mysql-5.5.15 - это папка куда мы ставимся, вы же не хотите убить ваш мускуль.
make
make install

Конфигурируем БД, логинимся под рутом и выполняем следующие действия:
install plugin test_plugin_server soname 'auth_test_plugin.so';
show plugins;
+-----------------------+--------+--------------------+---------------------+---------+
| Name                  | Status | Type               | Library             | License |
+-----------------------+--------+--------------------+---------------------+---------+
...
| test_plugin_server    | ACTIVE | AUTHENTICATION     | auth_test_plugin.so | GPL     |
+-----------------------+--------+--------------------+---------------------+---------+
create user 'mike'@'localhost';
create user 'mcshadow'@'localhost' identified with 'test_plugin_server' as 'volki';
grant proxy on 'root'@’localhost’ to 'mcshadow'@'localhost';
grant proxy on 'mike'@’localhost’ to 'mcshadow'@'localhost';
select * from mysql.proxies_priv;
+-----------+----------+-----------------+--------------+------------+----------------+---------------------+
| Host      | User     | Proxied_host    | Proxied_user | With_grant | Grantor        | Timestamp           |
+-----------+----------+-----------------+--------------+------------+----------------+---------------------+
...
| localhost | mcshadow | localhost       | root         |          0 | root@localhost | 2011-08-17 01:15:09 |
| localhost | mcshadow | localhost       | mike         |          0 | root@localhost | 2011-08-17 01:30:35 |
+-----------+----------+-----------------+--------------+------------+----------------+---------------------+

Вот собственно и все — теперь можно логинится в БД под своим аккаунтом с правами root или любого другого пользователя на кого резрешено проксирование. Как видно из таблицы можно ещё выставить with grant option, но сугубо ИМХО это уже лишнее.

Заключение


Пока конечно решение сыровато, ибо не понятно что работает, а что нет. У меня к примеру работало все что я пробовал, однако полной уверенности в нем пока нету. Для большего понимания можно почитать листинг юнит тестов — plugin_auth.result, который как оказывается есть в сорцах в папочке mysql-test\r (спасибо за подсказку svetasmirnova), а вообще очень мало инфы по теме GRANT PROXY. По итогам этих работ мы сделили diff -Nur и собираем RPM — попробуем погонять данное решение (после review настоящими программистами) на промышленной БД. Надеюсь не подведет (тьфу-тьфу-тьфу).
Изучайте! Присоединяйтесь! Делитесь опытом!
Tags:
Hubs:
Total votes 22: ↑21 and ↓1+20
Comments17

Articles