NRPE, root et xinetd

November 16, 2012 by nono
Catégories geekeries - Mots-clés Nagios système

Ami lecteur, si tu es, comme moi, admin système, je suis prêt à parier que tu as, comme moi, un système de monitoring que tu aimerais bien faire tomber en marche. Je m'en vais ici te donner une petite magouille toute simple qui le rendra un peu moins foireux.

La machine que je vais prendre pour illustrer mon exemple est une baie SAN (iSCSI plus précisément) qui tourne sous Scientific Linux 6. Comme toutes les autres machines de mon parc, elle fait tourner NRPE et elle est surveillée par un Nagios. Le but ici est de  surveiller l'état du service tgt, qui assure la fonction de cible iSCSI.

Première étape : interroger le service depuis la machine elle-même. Ça se fait au moyen de la commande suivante :

/usr/sbin/tgtadm -C 0 --op show --mode target

Cette commande retourne la liste des cibles iSCSI définies sur la machine accompagnée d'un certain nombre d'informations à propos d'icelles (informations que j'ignorerai ici, ce n'est pas mon propos). Le premier réflexe est donc d'encapsuler cette commande dans un script shell pour en faire un greffon Nagios rudimentaire :

#!/bin/bash

ok () {
    echo "TGT OK - $*"
    exit 0
}

critical () {
    echo "TGT CRITICAL - $*"
    exit 2
}

result=$(/usr/sbin/tgtadm -C 0 --op show --mode target)
if [ $? -eq 0 ]
then
    ok $(printf "$result" | egrep '^Target')
else
    critical $result
fi

Rien de bien compliqué, n'est-ce pas ? Si ce n'est que l'utilisateur qui fait tourner NRPE n'aura probablement pas le droit de lancer la commande tgtadm et encore moins d'en tirer quoi que ce soit d'utile. Ou alors c'est que ton système est administré bizarrement.

Comment faire pour passer outre ? Ton premier réflexe sera probablement le même que le mien : mais c'est bien sur, y'a qu'à configurer sudo comme il faut et roule la galère. Sauf que l'idée me plait moyennement ; pas que je n'aie pas confiance en sudo, mais je sais pertinemment que je suis capable de le configurer comme il ne faudrait pas. Du coup j'ai pensé à une meilleure approche : utiliser xinetd.

Concrètement, j'ai commencé par ajouter un service dédié à sa config :

service tgtstatus
{
        type            = UNLISTED
        disable         = no
        bind            = 127.0.0.1
        port            = 11112
        socket_type     = stream
        wait            = no
        user            = root
        server          = /usr/sbin/tgtadm
        server_args     = -C 0 --op show --mode target
        log_on_failure  += USERID
}

Tu t'empresseras de me signaler que n'importe quel utilisateur local peut, du coup, voir la configuration des cibles iSCSI. Je m'empresserai de te répondre que, d'une, c'est moins grave que de pouvoir exécuter n'importe quoi à travers une config sudo qui permet de lancer un script shell pas blindé comme il faut, et de deux, que de toute façon mes baies SAN n'ont pas d'utilisateurs locaux, manquerait plus que ça.

Du coup, l'appel à tgtadm dans mon greffon Nagios est remplacé par la ligne suivante :

result=$(/usr/bin/nc 127.0.0.1 11112)

et seul le morceau du greffon pour lequel c'est absolument nécessaire est exécuté par root, sans aucun moyen de lui passer des paramètres scabreux. C'est pas beau, ça ?

En fait, ce n'est pas si beau que ça : dans l'histoire, je viens de perdre le code de retour de tgtadm (que j'ai remplacé par celui de nc, qui est, tu en conviendras, un peu moins pertinent). Pour m'en tirer, j'ai donc dû interpréter la sortie de ma commande et pas son code de retour. Du coup j'ai laissé tomber le shell pour écrire ça dans un vrai langage de programmation, en l'occurrence Python ; voilà le résultat :

#!/usr/bin/env python
# Arnaud Gomes 2012

import sys, socket, re

# Le port du service xinetd
PORT = 11112

# Codes de retour Nagios
R_OK = 0
R_WARN = 1
R_CRIT = 2

status = ''
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', PORT))
while True:
    r = s.recv(1024)
    if not r:
        break
    status += r

if not status:
    print 'TGT WARNING - No targets defined'
    sys.exit(R_WARN)

if re.search(re.compile('^Target [0-9]+:', re.MULTILINE), status):
    output = ' '.join([re.sub('^Target [0-9]+:\s*', '', line)
                       for line in status.split('\n')
                       if re.match('^Target [0-9]+:', line)])
    print 'TGT OK - Targets %s' % output
    sys.exit(R_OK)

print 'TGT CRITICAL - %s' % status
sys.exit(R_CRIT)

Et cette fois-ci c'est bon, ça marche vraiment. :-)