При управлении большим парком серверов (100 и более) в определенный момент возникает вопрос об упрощении выполнения рутинных задач.
Одно из главных требований в таких условиях — иметь полное представление о том, что и когда происходит на серверах, находящихся в зоне личной ответственности, но доступ к которым имеют еще как минимум несколько десятков разработчиков.
Сегодня мы поговорим об авторизации пользователей на Linux-серверах с использованием БД MySQL и приложения Puppet.
Требования к системе и возможные решения
Итак, перед нами стояла задача внедрить централизованную систему управления доступом пользователей к серверам.
Безусловно, подобная система у нас уже была, и работала она вполне приемлемо, выполняла возложенные на нее задачи и в целом соответствовала нашим требованиям. Управление также было централизованным.
Но с течением времени стали появляться новые задачи, решать которые в существующих условиях становилось проблематично. И все чаще мы понимали: пора что-то менять в этой схеме. В конечном итоге было решено доработать текущую систему, чтобы она соответствовала нашим запросам.
В результате размышлений сотрудников IT-отдела появилось несколько идей:
- Использование директории LDAP в различных вариациях.
- Формирование списка пользователей в одном месте, выкладывание этого списка на все сервера (возможное управление доступом на уровне access.conf).
- LDAP в паре с Kerberos.
- Формирование пакетов (например, rpm) на основе некоего хранилища с дальнейшей установкой (например, как обновление пакета).
У каждой из идей были свои плюсы и минусы. Например, в случае в LDAP (или LDAP плюс Kerberos) нам пришлось бы изучать дополнительные сервисы, решать вопросы оптимизации хранения данных в директории LDAP, устанавливать дополнительные LDAP-proxy для снижения нагрузки на основной сервер (потому как нагрузки у нас действительно большие). При выборе вариантов 2 и 4 мы получили бы тот же инструмент, который, увы, перестал удовлетворять нашим потребностям.
Однажды в понедельник (после основательного отдыха и в силу хорошего настроения) появилась одна интересная мысль: в нашей работе мы используем приложение Puppet, в том числе и для выдачи прав и полномочий пользователям. Так почему же не использовать его как основной инструмент?
Тут и началось самое интересное: готового решения не было, но существовал перечень обязательных требований к нашей системе:
- При выборе пользователя получать список доступных ему хостов.
- Выдавать пользователю sudo-привилегии, которые могут быть разными в зависимости от хоста.
- Немедленный отзыв привилегий и доступа по требованию.
- Легкость управления (добавление, удаление, правка).
- Отсутствие необходимости создавать новые службы и сервисы, которые впоследствии придется изучать, поддерживать и сопровождать.
- Не привязывать сервера к внешним источникам авторизации, т.к. использовать локальную Unix-авторизацию надежнее и уже проверено временем.
И в качестве бонусов хотелось бы получить следующее:
- поддержка ключей rsa, dsa в неограниченном количестве;
- отслеживание изменений;
- поддержка templates для одинаковых задач (далее в статье описано, для чего это нужно);
- поддержка «регулярных выражений» в поле «Сервера, к которым есть доступ»;
- поддержка групп;
- поддержка квот.
В итоге было решено хранить структуры данных в MySQL (хранение в БД нам показалось удобным. Та структура, что есть в ней, может храниться и другими способами.), генерировать puppet-like манифесты «самописным» обработчиком, а также «приделывать» ко всему этому простой WEB UI. Забегая вперед, хочется сказать, что результат превзошёл наши ожидания, т.к. на этапе написания были добавлены важные и интересные моменты.
Реализация идеи
Сразу отметим, что мы имеем несколько географически удаленных площадок, где размещено оборудование; далее обозначим их «Платформа 1», «Платформа 2» и т.д. Обращаем ваше внимание, что приведенная ниже структура хранения данных показана только в качестве примера.
Сервера (узлы) в нашем примере будут иметь следующие обозначения:
- www — это веб-кластер, который включает в себя все узлы, например www1, www2… wwwN;
- www134 — это один конкретный узел из группы веб-кластера;
- www[15-17] — это узлы www15, www16, www17 из группы веб-кластера (используется для того, чтобы не писать их по очереди).
Структура базы данных
1. Таблица Users — основная таблица, содержащая следующую информацию:
- логин пользователя;
- ФИО пользователя;
- группы, к которым он принадлежит;
- командный интерпретатор (мы не против, чтобы пользователи использовали то, что им удобно);
- сервера, к которым пользователь должен иметь доступ;
- публичные ключи;
- квота на файловую систему (есть необходимость использовать на некоторых хостах);
- идентификаторы sudo-шаблонов, привязанных к пользователю.
2. Таблица UsersWithBigUid — аналогична предыдущей, но используется для заведения «пользователей для служебных нужд» (например, существует географически удаленный пользователь, который не должен у нас нигде фигурировать, но ему необходим доступ к конкретному серверу, узлу или узлам).
3. Таблица VpnOnlyUsers:
- логин пользователя;
- хосты, доступ к которым должен иметь пользователь через подключение по VPN.
4. Таблица TmplSudoersRules с описанием каждого правила sudo, входящего в тот или иной шаблон.
5. Таблица TmplSudoers, содержащая названия и комментарии к sudo-шаблонам.
6. Таблица SystemUsers — почти аналогична таблице с простыми пользователями, но используется для заведения пользователей с неограниченным правом доступа. В таблице присутствует дополнительное поле с идентификатором платформы на тот случай, если пользователь нужен только на одной из низ.
7. Таблица Sudoers содержит персонализированные правила sudo для пользователей.
8. Таблица Hostaliases состоит из алиасов имен серверов, которые в определенных случаях более удобны, чем имена, например, когда на один сервер возложено несколько ролей. С помощью данного функционала мы получаем еще один уровень абстракции и нам не важно имя узла.
Создание нового пользователя и его веб-интерфейса
1. Заполняем общую информацию о пользователе.
2. Выдаем доступ к серверам.
Примечание. Поля *servers — перечисление имен серверов и (или) групп через запятую. Пример заполнения данных полей:
- %chars — это группа, например, “www”;
- %chars%int — это конкретный узел, например, “www89”;
- %chars[%int-%int] — это диапазон узлов в определенной группе, например, “www[17-29]”.
3. Добавляем один или несколько sshpub-ключей.
4. Добавляем sudo-права пользователю.
После добавления пользователя и указания sudo-прав доступы обновятся на всех серверах в течение нескольких минут. Заведение пользователя на серверах можно ускорить, если в этом есть необходимость.
Давайте посмотрим, что нам нужно сделать для получения ACL для VPN. Все более чем просто:
1. Выбираем пользователя.
2. Открываем вкладку VPN, выбираем «Generate VPN». Получаем правило, которое нужно лишь добавить в RADIUS.
Примечание. Безусловно, данные сами по себе не появляются. В нашем случае обрабатываются поля со списком серверов, к которым пользователь имеет доступ. На основании этих данных и формируется VPN access-list. Здесь тоже предусмотрена определенная гибкость, т.е. мы можем выдать доступ как к 1 узлу в подсети, так и ко всей подсети. Дублирование отсутствует, т.е. если есть доступ в подсеть 10.11.12.0/24, то не имеет смысла добавлять что-либо наподобие “ip:inacl#N=permit ip any host 10.11.12.18”. В нашем примере полученные данные необходимо внести в конфигурацию авторизации VPN вручную (мы предлагаем вам самостоятельно поразмыслить, как это можно сделать — пусть это будет своего рода “домашним заданием”).
Несколько примеров (Задача — Решение)
Задача 1: отозвать весь доступ у пользователя.
Решение: очистить поля с указанием серверов у пользователя, после чего он автоматически удалится на всех серверах (домашняя директория не удаляется).
Задача 2: отозвать доступ к определенному серверу.
Решение: убрать этот хост в общем списке хостов пользователя.
Задача 3: предоставить доступ к определенному серверу (серверам).
Решение: дописать нужный хост(-ы) к разрешенным у пользователя.
Задача 4: дать возможность перезапуска nginx на машинах в www-кластере группе лиц.
Решение:
a. Заводим новый sudoers-template, даем ему адекватное имя и комментарий.
INSERT INTO `tmplsudoers` (`tmplname`, `comments`)
VALUES
('suwwwrun', 'su to wwwrun user and restart nginx if needed');
Примечание. Данный пример указан в качестве альтернативы нашему веб-интерфейсу.
b. Добавляем правила, характерные для данного шаблона.
INSERT INTO `tmplsudoersrules` (`tmplid`, `hostname`, `runas`, `commands`, `nopasswd`)
VALUES
(26, 'www[0-9]*', 'ALL', '/bin/su - wwwrun', 1);
INSERT INTO `tmplsudoersrules` (`tmplid`, `hostname`, `runas`, `commands`, `nopasswd`)
VALUES
(26, 'www[0-9]*', 'ALL', '/etc/init.d/nginx *', 1);
c. Теперь нам достаточно добавить данный шаблон пользователю в интерфейс, после чего у него заработает весь набор правил.
Примечание. Помимо шаблонных правил мы можем задавать уникальные правила отдельным пользователям.
Задача 5: обеспечить безопасное добавление sudo, поскольку в некоторых случаях безобидное, на первый взгляд, sudo может нанести ущерб безопасности (например, на less, mount или rsync). Разрешить службе HelpDesk давать такие права сотрудникам.
Решение: шаблоны составляют опытные системные администраторы, а сотрудники HelpDesk только ставят соответствующие отметки.
Разрешить сотрудникам HelpDesk назначать шаблоны, но запретить их корректировку, а также запретить выдачу уникальных правил определенному пользователю.
Задача 6: сформировать sudoers таким образом, чтобы не нарушить работу сервера(-ов), потому что наличие некорректного синтаксиса и (или) дублирование директив HOSTALIASES может иметь плохие последствия.
Решение:
a. Перед добавлением или изменением sudoers-файла для пользователя производится проверка синтаксиса (в случае наличия ошибки формируется уведомление, файл sudoers для пользователя не изменяется).
Пример функции на Python:
VISUDO ='/usr/sbin/visudo'
def visudoCheck(filename,user):
visudo_cmd = 'echo -ne "%s "; echo "%s" | %s -c -f -' % (user,filename,VISUDO)
(visudo_status, visudo_output) = commands.getstatusoutput(visudo_cmd)
if not visudo_status == 0:
print "n"+filename
sys.stderr.write(visudo_output[:1024])
return visudo_status
b. Шаблоны составляют опытные системные администраторы, а сотрудники HelpDesk только ставят соответствующие отметки.
c. При формировании строк “Host_Alias” используются следующие правила во избежание дублирования:
- если это не шаблон, а «specific-rule», то берется такая маска: “STRING%USERNAME%%USERID%”
- если это шаблон, то берется такая маска: “STRING%USERNAME%%TMPLRULEID%”, где %TMPLRULEID% — id записи правила.
Другие задачи решаются так же просто, ваши вопросы мы можем обсудить в комментариях.
Принцип работы схемы
- Ввод и (или) изменение данных происходит через пользовательский веб-интерфейс.
- Сron запускает задачу, которая формирует Puppet manifest для нужной нам платформы.
- Если на втором этапе появляется изменение в правиле, то делается commit в репозиторий Git, который хранит данные и сообщает обо всех изменениях.
- На сервере с Puppet осуществляется проверка git-репозитария, и как только становится понятно, что есть обновления и (или) изменения – выполняется fetch с новым конфигом.
Скрипт запускается лишь с одним параметром – идентификатором платформы, для которой формируются правила. После выполнения получаем подобный результат:
class virtualusers {
@group { "wwwaccess": gid => 1001, ensure => present }
@user { "petek":
ensure => $hostname ? {
simplehostname1 => present,
/hostgroup1(d.*)$/ => present,
/ hostgroup2(d.*)$/ => present,
/ hostgroup3(d.*)$/ => present,
…
…
default => absent,
},
comment => Petek Petkovich',
gid => '1001',
home => '/home/petek',
uid => '1018',
password => '*',
shell => '/bin/bash',
}
@virtualuser_key { "petek":
group => '1001',
name => 'petek',
key =>"ssh-rsa dqwedqwedqwedqedqwedqwedqwedqwedqewdqwed=",
require => User["petek"];
}
@exec { "petek_quota":
command => "/usr/sbin/setquota -u greli 5242880 5242880 0 0 -a /filesystem/",
path => "/usr/sbin",
onlyif => "/usr/bin/test `/usr/sbin/repquota -ua | /usr/bin/egrep '^peteks*' | /usr/bin/awk {'print $4'}` -ne "5242880"",
}
@file { "/etc/sudoers.d/1018_petek" :
ensure => present,
owner => 'root',
mode => 0440,
content => '';
}
if $hostname =~ /^hostgroup1d*$/ {
realize (Virtualuser_key['petek',’petek2’], File['/etc/sudoers.d/1018_petek', '/etc/sudoers.d/1020_petek2',])
realize (Exec['petek_quota'])
}
}
Для примера и читабельности оставлен только кусок манифеста. По правде говоря, читабельность манифестов нас не сильно интересует, в силу того что сразу после их формирования происходит проверка синтаксиса. Если он в полном порядке, то puppet master способен его прочитать, в таком случае нам незачем вмешиваться в процесс.
Приведем пример проверки синтаксиса:
PUPPET='/usr/bin/puppet'
def puppetparsercheck(filename):
puppet_cmd = '%s parser validate %s' % (PUPPET, filename)
(puppetparser_status, puppetparser_output) = commands.getstatusoutput(puppet_cmd)
print str(puppet_cmd)
if not puppetparser_status == 0:
sys.stderr.write(puppetparser_output[:1024])
return puppetparser_status
Дополнительные плюсы реализации
- На каждом отдельном узле действуют только те правила sudo, которые ему соответствуют; посмотреть их можно, набрав sudo -l.
- Для удаления пользователей не нужно осуществлять отдельных действий, т.к. если у пользователя нет доступа, то он по умолчанию “ensure => absent” (на языке Puppet).
Таким образом, мы получили средство управления доступом пользователей к серверам, соответствующее всем нашим требованиям. Оно использует инструментарий, существовавший до момента его написания (что само по себе замечательно), а также избавило нас от внедрения и поддержки дополнительных сервисов.
И самое главное, пожалуй, то, что с такой системой способен справиться любой сотрудник «первой линии поддержки» (те, кто помогает с простыми просьбами пользователей).
Автор: Badoo