Управление правами доступа к WMI через Puppet

в 20:19, , рубрики: linux, opsview, powershell, puppet, ruby, WMI, автоматизация, Серверное администрирование, системное администрирование, метки: , , , , , ,

Управление правами доступа к WMI через Puppet

В качестве предисловия

Основной задачей моей работы является поддержка парка железных и vm хостов — уже под 200 (а приходил было менше 100, эх, время бежит...) Поддерживаю все железо, а также сеть. Также на мне весь мониторинг (используем Opsview — сделан на ядре nagios), аггрегация логов (я внедрил Logstash, обалденное opensource решение за место ну ооочень дорогого Splunk), configuration management (puppet), бекапы, поддержка баз данных и прочих систем тоже на мне (MongoDB, MySQL, Redis, ElasticSearch, etc). В общем — все самое интересное). Стоит отметить что у нас достаточно тонкая грань между поддержкой и разработкой, и разработчики часто говорят что они хотят, а я уже занимаюсь внедрением. Хочется рассказать обо всем что происходит интересного и какие технологии удается использовать. Какие прижились, а какие по каким-то причинам нет.

В свободное от решения проблем время перевожу инфраструктуру на Infrastructure-as-a-code (IaaC), выбрал puppet для этого из-за неоднородности нашей инфраструктуры. В моей сети зоопарк из Windows Server 2008, Windows Server 2012, CentOS 5.5, CentOS 6.4. Ах, да, пару дедушек на 2003 — пора их на пенсию отправлять скоро…

Я уже писал о том, как я использую Puppet для автоматической настройки мониторинга в Opsvew, а сегодня хочу поговорить о том, как я в очередной раз «боролся» с гетерогенностью моей среды.

Задача

Возникла необходимость автоматизировать конфигурацию WMI на серверах Windows 2008 / 2012. Ключевой необходимостью стало добавление сервисного пользователя (назовем его «domainservice-user») в локальные группы сервера, которые разрешают удаленное использование WMI, а также доступ к Performance Counters, Performance Logs, в общем ко всему что нужно чтобы удаленно мониторить сервер. Сами группы определились достаточно быстро, оставалось найти удобный и быстрый способ это сделать. Также необходимо было дать права пользователю domainservice-user на доступ к корневым неймспейсам WMI. Так же все это должно быть частью общей концепции IaaC, что должно означать как минимум проверку текущего состояния, и пропускать выполнение если пользователь уже добавлен куда нужно в любом варианте присутствия-отсутствия пользователя в группах. Т.е. решение должно быть максимально автоматизированным, а точнее полностью. После небольшого гугления стало ясно что для моего случая нужно, а мне предстояло:

Добавить доменного пользователя domainservice-user в локальные группы (как минимум):
— Certificate Service DCOM Access
— Performance Log Users
— Performance Monitor Users
— Distributed COM Users.
Установить права доступа, как минимум, «read» для пользователя domainservice-user на следующие неймспейсы WMI:
— CIMV2
— MicrosoftIISv2.
Как только все будет установлено, check_wmi_plus (входит в стандартную поставку Opsview Pro) сможет получить необходимые данные об IIS и других интересных параметрах (а это то нам и надо)!

Сложности

Главная сложность которая возникла — готового решения чтобы запустить и быть уверенным в том, что «Сервер будет доступен для мониторинга» нет. В общем-то я и не очень расстроился, потому что очень редко бывают готовые решения под мои задачи, а если и бывают то часто делают что-то не то или не так.

Puppet имеет встроенный ресурс «user», который, по идее, должен был выполнить половину всех задач, не заработал в связке «доменный пользователь — локальная группа». Как оказалось это известный баг и его собираются исправлять (UPD: уже исправили в релизе 3.4), но постоянно отодвигают в следующий релиз puppet. Попытка выполнить workaround в puppet DSL не увенчалась успехом из-за слишком сложной структуры, требующей сложных escape последовательностей, которые не всегда работают.

Еще одна сложность — в windows отсутствует встроенный универсальный способ управления правами доступа к wmi классам, который можно было бы «обернуть» в puppet, если только ковырять реестр и изобретать велосипед.

Реализация

В итоге я принял решение написать свой провайдер и использовать его, до тех пор пока родной провайдер для добавления доменного пользователя в локальную группу на сервере не будет починен. И сделал я это… обернув powershell код!

win_user.rb

Puppet::Type.type(:win_user).provide(:win_user) do
  @doc = %q{Manage windows users and groups}
  desc "Manage windows users and groups"
  confine    :operatingsystem => :windows
  defaultfor :operatingsystem => :windows
	

	commands :powershell =>
	if File.exists?("#{ENV['SYSTEMROOT']}\sysnative\WindowsPowershell\v1.0\powershell.exe")
	  "#{ENV['SYSTEMROOT']}\sysnative\WindowsPowershell\v1.0\powershell.exe"
	elsif File.exists?("#{ENV['SYSTEMROOT']}\system32\WindowsPowershell\v1.0\powershell.exe")
	  "#{ENV['SYSTEMROOT']}\system32\WindowsPowershell\v1.0\powershell.exe"
	else
	  'powershell.exe'
	end

  def add_user_to_group
    #
  end

  def exists?
    #add transform to array if just a string
    
    groups = resource[:groups]
    if groups.kind_of?(String)
      groups.to_a 
    end   
    found = false

    groups.to_a.each do |group|
      Puppet.debug("Checking the existence of value: #{self} in #{group}")
      result = powershell 'if (([ADSI]"WinNT://$env:computername/'+group+'").IsMember(([ADSI]"WinNT://$env:userdomain/'+ resource[:name]+'").ADsPath) -eq $False){echo 0} else {echo 1}'      
      
      if result.chomp == "0" #if not found (ps returned false on search of user in group)
        Puppet.debug("User '#{resource[:name]}' is not found in group '#{group}'")
        found = false #
        break
      else
        Puppet.debug("User '#{resource[:name]}' is found in a group '#{group}'")
        found = true
      end
    end
    found
  end

  def create
    groups = resource[:groups]
    if groups.kind_of?(String)
      groups.to_a 
    end

    groups.to_a.each do |group|
      Puppet.debug("Adding user to a group #{self}")
      powershell 'if (([ADSI]"WinNT://$env:computername/'+group+'").IsMember(([ADSI]"WinNT://$env:userdomain/'+ resource[:name]+'").ADsPath) -eq $False){$([ADSI]"WinNT://$env:computername/'+group+'").Add(([ADSI]"WinNT://$env:userdomain/'+resource[:name]+'").ADsPath)} else {echo "User is already in a group"}'
    end
  end

  def destroy
    
  end
end

Для настройки параметров WMI пришлось пользоваться сторонней opensource утилитой wmisecurity.exe. Для ее установки я создал пакет на chocoaltey.org — wmisecurity. Для установки пакета я использовал puppet-провайдер chocolatey, который я использую постоянно.

И вот сам puppet-манифест, который использует раннее написанный модуль, а также содержит powershell хуки для добавления прав доступа к wmi классам для пользователя (возможно перепишу это как отдельный модуль позднее):

wmi.pp

class packages::wmi {	
	
	$wmiuser = 'service-user'
	
	###Doesn't work on windows right now
	#user { $wmiuser:
	#		groups => ['Certificate Service DCOM Access','Performance Log Users','Performance Monitor Users', 'Distributed COM Users'],
	#	}

	
    win_user { $wmiuser:
      groups => ['Certificate Service DCOM Access','Performance Log Users','Performance Monitor Users', 'Distributed COM Users'],
      ensure => present_local,
    }

	###it is required to add user to those local groups in order monitoring to perform correctly.
	
	exec {"add-to-wmi-cimv2":
		command => "wmisecurity.exe /C=$env:computername /A /N=Root/CIMV2 /M=$env:userdomain\$wmiuser:REMOTEACCESS /R",
		path => $::path,		
		#if found user guid - skip
		onlyif => "if (WmiSecurity.exe /c=$env:computername /N=Root/CIMV2 /R | Select-String $($(New-Object System.Security.Principal.NTAccount(
		"$env:userdomain", '$wmiuser')).Translate([System.Security.Principal.SecurityIdentifier]).Value)){exit 1} else {exit 0}",
		provider => powershell,
		require => Package['wmisecurity'],	
	}	
	
	exec {"add-to-wmi-microsoftiisv2":
		command => "wmisecurity.exe /C=$env:computername /A /N=Root/MicrosoftIISv2 /M=$env:userdomain\$wmiuser:REMOTEACCESS /R",
		path => $::path,		
		#if found user guid - skip
		onlyif => "if (WmiSecurity.exe /c=$env:computername /N=Root/MicrosoftIISv2 /R | Select-String $($(New-Object System.Security.Principal.NTAccount(
		"$env:userdomain", '$wmiuser')).Translate([System.Security.Principal.SecurityIdentifier]).Value)){exit 1} else {exit 0}",
		provider => powershell,
		require => Package['wmisecurity'],	
	}
	
	package {'wmisecurity':
		provider => 'chocolatey',
		install_options => '-pre',		
		require         => Class["packages::chocolatey"]
	}
}

Заключение

Конечно, сам модуль далек от идеала, и не хватает много чего, код явно грязный, но он работает и выполняет то, что задумывалось. Рефакторинг планируется в следующей итерации, и думаю это случится когда выйдет 3.4. Вот идеальный вариант манифеста, который я себе представляю (для тех, кто будет ругаться про «грязный код, который работает»):

wmi.pp

class packages::wmi {	
	
	$wmiuser = "${env:userdomain}\service-user"	
	
	
    user { $wmiuser:
      groups => ['Certificate Service DCOM Access','Performance Log Users','Performance Monitor Users', 'Distributed COM Users'],
      ensure => present,
    }
	
	wmi_security_user { $wmiuser:
		namespaces => ['Root/CIMV2','Root/MicrosoftIISv2'],
		ensure => present,
	}
}

Теперь при настройке нового сервера мне остается назначить класс packages::wmi на этот сервер (в ручную или через include) и все, puppet сделает свое дело. Лично я чаще всего использую этот класс через класс opsview, который автоматически создает хост для мониторинга в opsview и назначет нужные шаблоны, т.е. если это, скажем, сервер c IIS, итоговый puppet-класс сообщит opsview всю необходимую информацию о том, что это хост с IIS, с такими-то и такими-то хостами которые нужно тоже мониторить определенным образом, а также назначит в opsview шаблон мониторинга через wmi, который зависит от класса который мы описали выше. Вот так, вроде ничего не упустил.

Пара скриншотов результата:

Управление правами доступа к WMI через Puppet
Управление правами доступа к WMI через Puppet

Автор: kireevco

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js