Pull to refresh

Защищённый SMS Commander

Reading time9 min
Views6.8K
Большинство системных администраторов так или иначе мониторящих оборудование компании, знакомы с пакетом smstools, зачастую используемого лишь для отправки SMS сообщений о состоянии инфраструктуры. Оборотной стороной медали является необходимость удалённого управления инфраструктурой предприятия при недоступности VPN подключения или нахождении вне зоны доступа в интернет. Ранее описанные способы управления безусловно имеют право на жизнь, но рассчитаны на управление одним сервером и не предоставляют возможности безопасного выполнения команд. Ниже по тексту я предлагаю вашему вниманию рабочий механизм безопасного подтверждения выполнения sms команд внутри локальной сети.

Начнём с того «Зачем это нужно?»


Согласно правилу «Правильно настроенная система в обслуживании не нуждается», правильные сисадмины не перенастраивают стабильно работающее оборудование ограничиваясь только установкой обновлений.
Но в работе серверов бывают моменты когда вешается какой-нить процесс обеспечивающий работу сетевых шар и RPC, скажем LanManServer(в простонародии server), причём вешается не сам процесс, а какая-то его ветка. В итоге мы имеем сервер, который нельзя перезапустить через удалённое управление или корректно погасить.

Что можно с этой проблемой сделать ?


Почти все современные сервера имеют на борту IPMI и возможность управления питанием. Отсюда и будем строить наш функционал. Если ваши сервера дополнительно мониторятся через nagios или аналогичные пакеты, то в большинстве случаев у вас используется nrpe -Nagios Remote Plugin Executor, а это значит у вас есть независимый от системных служб механизм по управлению сервером. Достаточно в конфиге NSClient++ прописать часто используемые команды типа reboot, shutdown, stop_server, start_server и удалённо делать с сервером необходимые действия.
В старом конфиге клиента вызов внешних скриптов расположен в разделе "[External Scripts]", в новом "[/settings/external scripts/scripts]". Текстовка в обоих случаях одинаковая.

reboot=scripts\s_reboot.cmd
shutdown=scripts\s_shutdown.cmd
stop_server=scripts\stop_server.cmd
start_server=scripts\start_server.cmd


текстовка скриптов
s_reboot.cmd
@echo "Reboot initiated"
@start cmd /c shutdown -r -f -t 00
@exit 0


s_shutdown.cmd
@echo "Shutdown initiated"
@start cmd /c shutdown -s -f -t 00
@exit 0


start_server.cmd
@start cmd /c net start server
@if %ERRORLEVEL% EQU 0 goto ok
@echo Some problem with service start
@exit 1
:ok
@echo Service started
@exit 0


stop_server.cmd
@start cmd /c net stop server /y
@if %ERRORLEVEL% EQU 0 goto ok
@echo Some problem with service stop
@exit 1
:ok
@echo Service stopped
@exit 0


Это базовый список команд которые можно использовать удалённо. Можно вызывать остановку или поднятие сервиса передавая в аналогичный скрипт аргументы. Например передёрнуть IIS.

Вызов команды обычно прост и выглядит как
/usr/local/libexec/nagios/check_nrpe2 -H <HOST> -c <command> -a <arguments>


С nrpe вроде бы разобрались, теперь нужно обслужить IPMI.
Для диалога с IPMI системой сервера можно воспользоваться пакетом ipmitool, который представляет собой интерфейс доступа к управлению ipmi из командной строки.

Основной набор команд который нам необходим, относится к разделу «power», а именно
ipmitool -H <host> -U <username> -P <password> power status - узнать состояние питания на сервере
ipmitool -H <host> -U <username> -P <password> power up - включить сервер
ipmitool -H <host> -U <username> -P <password> power off - выключить питание
ipmitool -H <host> -U <username> -P <password> power reset - эмулировать нажатие клавиши reset


Можно еще почитать температурные сенсоры, скорость вращения вентиляторов, но это нам не особо нужно.
С основным набором команд разобрались.

Как обеспечить безопасность управления ?


В связи с тем, что последнее время появилось некоторое количество сервисов позволяющих подделать номер отправителя смс — обычная проверка номера отправителя стала недостаточно надёжной.
Пересылка прямого пароля в сообщении тоже вещь ненадёжная, т.к. перехват сообщения позволит получить полный доступ к вашей системе управления. Что же делать?

Выход на удивление прост. OTP (One Time Passwords). С точки зрения простоты использования и отсутствии зависимости генерируемых паролей от времени оптимальным решением будет использование OPIE.

Почему OPIE ?


Если у вас есть несколько системных администраторов или инженеров обслуживающих сервера, то выдавать каждому пароль и начальный вектор инициализации будет не совсем корректным. Для обслуживания серверов уместно выдать каждому список паролей из различных блоков.

# opiekey -5 -n 5 5 habrahabr
Using the MD5 algorithm to compute response.
Reminder: Dont use opiekey from telnet or dial-in sessions.
Enter secret pass phrase:
1: JIG RIFT BODE OLGA RICK JAG
2: RIM HIVE BANG LIMA HELL OMEN
3: BULB MOD CARR BANK MOS SET
4: GARB BAWL MANY HAL GLOW FEED
5: FAWN EDGY MEET SHUT LIKE TIME

# opiekey -5 -n 5 25 habrahabr
Using the MD5 algorithm to compute response.
Reminder: Dont use opiekey from telnet or dial-in sessions.
Enter secret pass phrase:
21: MAIN HOFF JAM OATH SMOG LIED
22: FUND DENY BYTE BOLT NIBS EASY
23: SLY COAT FLEA CAGE MAE COAL
24: SURE LEFT HULK CLAN SHUN DAR
25: GRAB LIE CLAN FLAK MEL ROSE


А в случае использования блоков паролей можно точно утверждать чьей рукой был выключен или перезагружен сервер. С точки зрения использования системы «в одно лицо», для Android телефонов, целесообразнее будет установить пакет OTPDroid для онлайн генерации паролей. Прекрасен он тем, что в буфер обмена сразу копируется значение сгенерированного пароля и трёх нажатий хватает для копипаста пароля в sms.

Вы спросите «А как же двойное использование одного и того же пароля ?»
Ответ прост: «Нужно просто контролировать и вести список использованных последовательностей прилетевших в смс»

Как всё это заставить работать вместе ?

Первым делом в конфигурационном файле smsd необходимо добавить вызов обработчика событий.
eventhandler = /usr/local/etc/smshandlers/smsevent


Программа обработки представляет собой обычный shell скрипт.

Далее пойдёт много shell кода smsevent
#!/bin/sh

NRPETOOL="/usr/local/libexec/nagios/check_nrpe2"
IPMITOOL="/usr/local/bin/ipmitool"
FPINGTOOL="/usr/local/sbin/fping"
PARAMFILE="/usr/local/etc/smshandlers/param.list"
OPIEKEYBIN="/usr/bin/opiekey"
OPIESEED="HABROPIEProc"
OPIEPASS="StrongOPIEPassword"
OPIEUSEDSEED="/var/tmp/used.opie"
SMSDOUTSPOOL="/var/spool/sms/outgoing"

if [ ! -f ${OPIEUSEDSEED} ]; then
        echo "#USED SEEDS FILE. Do not EDIT" > ${OPIEUSEDSEED}
fi

if [ ! -w ${OPIEUSEDSEED} ]; then
        echo "file ${OPIEUSEDSEED} is not writable"
        exit
fi
if [ ! -r ${PARAMFILE} ]; then
        echo "file ${PARAMFILE} is not readable"
        exit
fi

IFS="
"
FOUNDNUM=""
NUMBER=""

# search any properties
search (){
# $1 - section [name] from param.list
# $2 - search string in section
SECTION=$1
SEARCHSTR=$2
shift
shift
OLDIFS=${IFS}
IFS="
"
        [ "X"${SEARCHSTR} == "X" ] && exit
        for i in ${PARAMS}; do
                # search section definition
                if [ `echo ${i} | grep -e "^\["` ]; then
                        CURSECTION=${i}
                        continue
                fi
                if [ "${CURSECTION}" == "["${SECTION}"]" ]; then
                        if [ `echo ${i} | grep -e "^\b${SEARCHSTR}\b:"` ]; then
                                echo ${i}
                                break
                        fi
                fi
        done
IFS=${OLDIFS}
}

cut_message_body (){
# split sms file to parts
# BSD buggy "grep -m 1 -A 10 -e '^$'" replacement
#
        FILE=$1
        NULLSTR=0
        for i in `cat -e $1`; do
                if [ "X"${i} == "X\$" ]; then
                        NULLSTR=1
                        continue
                fi
                if [ ${NULLSTR} -eq 1 ]; then
                        echo ${i}
                fi
        done
}

process_command () {
        CMD=$1
        shift
        CMDPARAM=$*
        oIFS=$IFS
        IFS=" "
        for host in ${CMDPARAM}; do
                HOSTACCOUNT=$( search hosts "${host}" )
                if [ "X"${HOSTACCOUNT} != "X" ]; then
                        ACCOUNT=`echo ${HOSTACCOUNT} | cut -d ":" -f 2-`
                        LOGINPW=$( search logins "${ACCOUNT}" )
                        if [ "X"${ACCOUNT} != "X" ]; then
                                if [ "${ACCOUNT}" != "null" ]; then
                                        USER=`echo ${LOGINPW} | cut -d ":" -f 2`
                                        PASSWORD=`echo ${LOGINPW} | cut -d ":" -f 3`
                                else
                                        USER="USER"
                                        PASSWORD="PASSWORD"
                                fi
                        else
                                echo "ACCOUNT ${ACCOUNT} NOT FOUND!"
                                break
                        fi
                else
                        echo "HOST ${host} NOT FOUND!"
                        continue
                fi
                opierequired=$( search commands_template "${CMD}" | cut -d ":" -f 2 )

                commandtemplate=$( search commands_template "${CMD}" | \
                                                                        cut -d ":" -f 3- | sed \
                                                                        -e "s@\%HOST\%@${host}@g" \
                                                                        -e "s@\%FPING\%@${FPINGTOOL}@g" \
                                                                        -e "s@%IPMITOOL%@${IPMITOOL}@g" \
                                                                        -e "s@%CHECK_NRPE%@${NRPETOOL}@g" \
                                                                        -e "s@%USER%@${USER}@g" \
                                                                        -e "s@%PASSWORD%@${PASSWORD}@g" )
                if [ "${opierequired}" -eq "1" ]; then
                        if [ "${OPIERES}" == "True" ]; then
                                echo "OPIE Success"
                                res=$( eval ${commandtemplate} )
                                sendsms ${NUMBER} ${host} ${res}
                        else
                                echo "OPIE Failed"
                                sendsms ${NUMBER} ${host} "OPIE Challenge failed"
                        fi
                else
                        res=$( eval ${commandtemplate} )
                        sendsms ${NUMBER} ${host} ${res}
                fi
        done
        IFS=$oIFS
}

sendsms () {
        NM=$1
        shift
        TMPFILE=`mktemp ${SMSDOUTSPOOL}/smscmd.XXXXXX` || exit 1
        echo "To: ${NM}" >> ${TMPFILE}
        echo >> ${TMPFILE}
        echo "$*" >> ${TMPFILE}
}

opiecheck () {
        OPIESEQ=`echo $* | cut -d " " -f 1 | sed -e 's/[^0-9]//g'`
        OPIESTR=`echo $* | cut -d " " -f 2-`
        if [ "X"${OPIESEQ} == "X" ]; then
                echo "OPIE SEQUENCE FOR NUMBER ${NUMBER} IS NOT SET!"
                OPIERES="False"
                break
        fi
        if [ `grep "^${OPIESEQ}$" ${OPIEUSEDSEED}` ]; then
                echo "USED OPIE SEQUENCE ${OPIESEQ} DETECTED"
                OPIERES="False"
                sendsms ${NUMBER} "OPIE SEQUENCE ${OPIESEQ} ALREADY USED"
                exit 1
        else
                echo ${OPIESEQ} >> ${OPIEUSEDSEED}
        fi
        OPIEGEN=`echo ${OPIEPASS} | ${OPIEKEYBIN} -5 -a ${OPIESEQ} ${OPIESEED} 2>/dev/null`
        if [ "X"${OPIESTR} != "X"${OPIEGEN} ]; then
                OPIERES="False"
        else
                OPIERES="True"
        fi
}


parse_commands () {
        for command in $*; do
                command=`echo ${command} | sed -e 's/\$$//g' -e 's/^\#//g'`
                CURCOMMAND=`echo ${command} | cut -d " " -f 1`
                CMDPARAM=`echo ${command} | cut -d " " -f 2-`
                        case "${CURCOMMAND}" in
                                "OPIE")
                                        opiecheck ${CMDPARAM}
                                        continue
                                        ;;
                                *)
                                        if [ `echo ${FOUNDNUM} | cut -d : -f 2 | grep "\b${CURCOMMAND}\b"` ]; then
                                                TMPMSG="ALLOWED COMMAND FOR NUMBER "${NUMBER}" - COMMAND: "${CURCOMMAND}" ARGS "${CMDPARAM}
                                                echo ${TMPMSG}
                                                process_command ${CURCOMMAND} ${CMDPARAM}
                                        else
                                                TMPMSG="DISALLOWED COMMAND FOR NUMBER "${NUMBER}" - COMMAND: "${CURCOMMAND}" ARGS  "${CMDPARAM}
                                                echo ${TMPMSG}
                                        fi
                                        ;;
                        esac
        done
}



PARAMS=`cat ${PARAMFILE} | grep -E -v '^#'`

if [ $1 == "RECEIVED" ]; then
        FILENAME=$2
        NUMBER=`grep 'From:' ${FILENAME} | sed -e 's/^From\:\ //g' -e 's/[^0-9]//g;'`
        FOUNDNUM=$( search phones ${NUMBER} )
        if [ "X"${FOUNDNUM} != "X"  ]; then
                commands=$( cut_message_body ${FILENAME} )
                parse_commands ${commands}
        fi
elif [ $1 == "SENT" ]; then
        echo $@
else
        echo "UNKNOWN STATUS $@"
fi


Немного комментариев о файле обработчика smsevent

В начале файла задаются основные переменные о нахождении файла конфигурации, программ проверки и управления, а также OPIE параметры для проверки паролей.

Немного конфигурационного файла param.list
# ---------------------------------------
#
# ALLOWED PHONES
# PHONENUMBER:COMMAND,COMMAND,COMMAND
# ---------------------------------------
[phones]
79030000000:RESET,PWROFF,PWRON,REBOOT,SHUTDOWN,STOPSERVER,STARTSERVER,ALIVE,PWRSTAT
79160000000:ALIVE,PWRSTAT
79020000000:PWRON,STARTSERVER,ALIVE
# ---------------------------------------
#
# HOST LIST
# REAL_IP:IPMI_USER
# ---------------------------------------
[hosts]
192.168.55.2:ADMSM
192.168.54.2:null
192.168.55.5:ADMDEF
192.168.54.5:null
# ---------------------------------------
#
# IPMI LOGINS
# IPMI_ACCOUNT:IPMI_NAME:IPMI_PASSWORD
# ---------------------------------------
[logins]
ADMSM:ADMIN:PW1234
ADMDEF:ADMIN:ADMIN
ADMIBM:USERID:USERID
# ---------------------------------------
#
# COMMANDS TEMPLATE
# COMMAND:OPIE_CHECK:COMMAND_TEXT
# ---------------------------------------
[commands_template]
RESET:1:%IPMITOOL% -H %HOST% -U %USER% -P %PASSWORD% power reset
PWROFF:1:%IPMITOOL% -H %HOST% -U %USER% -P %PASSWORD% power off
PWRON:0:%IPMITOOL% -H %HOST% -U %USER% -P %PASSWORD% power on
PWRSTAT:0:%IPMITOOL% -H %HOST% -U %USER% -P %PASSWORD% power status
REBOOT:1:%CHECK_NRPE% -H %HOST% -p 5666 -c reboot
SHUTDOWN:1:%CHECK_NRPE% -H %HOST% -p 5666 -c shutdown
STOPSERVER:1:%CHECK_NRPE% -H %HOST% -p 5666 -c stop_server
STARTSERVER:0:%CHECK_NRPE% -H %HOST% -p 5666 -c start_server
ALIVE:0:%FPING% %HOST%


В конфигурационном файле вы задаёте телефоны и список доступных им команд, список адресов серверов, логины и пароли IPMI для различных хостов, темплейты выполняемых команд.

В блоке темплейтов команд второй аргумент имеет значение 0 или 1 и либо пропускает проверку OPIE для команды, либо проверяет OPIE.

Самый простой смс запрос пришлёт обратно поочередно 2 sms с результатами проверки доступности хостов:

ALIVE 192.168.54.2 192.168.54.5


Запрос вида отправит хост 192.168.55.2 в перезагрузку путём эмуляции кнопки RESET, а хост 192.168.54.5 выключит штатным образом:

OPIE 1 COT NORM TILL JILL MOST MESH
RESET 192.168.55.2
SHUTDOWN 192.168.54.5


Все выполняемые посредством sms действия пишутся в расширенный лог smsd.

В общем-то, всё. Редактируете под себя конфиг, настраиваете сервера на приём команд и можно ехать с телефоном на отдых. Если вдруг что повиснет, выстрел прямой наводкой посредством sms сделает своё дело.

PS: Скрипт немного сыроват, но как говорится «совершенству нет предела!».
Tags:
Hubs:
Total votes 7: ↑7 and ↓0+7
Comments0

Articles