CEPH – Perdre 2 serveurs sur 3

Expérience vécue !

Contexte: Chez OVH, nous avons 1 cluster ceph composé de 3 serveurs. Sur chaque serveur, 1 MON, 2 OSD de 2To chaque.
Au total, nous avons 3 MON et 6 OSD, et plusieurs VM KVM utilisent le ceph en backend storage + vRACK OVH pour les flux privés.

Nous décidons de résilier 2 serveurs et de migrer toutes les VM en local pour des raisons diverses (on se concentre sur des containers et on réparti notre infra entre OVH et SCW pour la redondance).

Le jour J arrive, nous perdons les 2 serveurs et surprise, nous avons oublié 1 VM sur le cluster ceph.

*Panique*

Sur le seul serveur restant du cluster, la commande de status hang :

 ceph -s 

Nous avons un REPLICA à 3, on se dit tout n’est pas perdu ! *théoriquement*

1. Nous décidons de reconfigurer la mon map pour déclarer 1 seul MON

– On extrait la monmap active:

# systemctl stop ceph-mon@inxovh-hy001
# ceph-mon -i inxovh-hy001 --extract-monmap /tmp/monmap

– On supprime les 2 MON DOWN

# monmaptool  /tmp/monmap --rm inxovh-hy002
# monmaptool  /tmp/monmap --rm inxovh-hy003

– On réinjecte la monmap :

# ceph-mon -i inxovh-hy001 --inject-monmap /tmp/monmap

– On peut démarrer le mon survivant:

systemctl start ceph-mon@inxovh-hy001

– Ouf ceph -s répond désormais

# ceph -s
cluster:
    id:     0054ab1b-7efb-4456-8e29-db1213048fd6
    health: HEALTH_WARN
            noout flag(s) set
            4 osds down
            2 hosts (4 osds) down
            Reduced data availability: 200 pgs inactive, 200 pgs down
            Degraded data redundancy: 200 pgs unclean
data:
    pools:   3 pools, 200 pgs
    objects: 168k objects, 674 GB
    usage:   675 GB used, 1292 GB / 1968 GB avail
    pgs:     100.000% pgs not active
             200 down

C’est pas glorieux, tous nos PG sont down.

– Les OSD morts sont encore vus UP :

root@inxovh-hy001 (node1):~# ceph osd tree
ID CLASS WEIGHT  TYPE NAME             STATUS REWEIGHT PRI-AFF
-1       6.00000 root default
-2       2.00000     host inxovh-hy001
 4   hdd 1.00000         osd.4             up  1.00000 1.00000
 5   hdd 1.00000         osd.5             up  1.00000 1.00000
-3       2.00000     host inxovh-hy002
 0   hdd 1.00000         osd.0             up  1.00000 1.00000
 1   hdd 1.00000         osd.1             up  1.00000 1.00000
-4       2.00000     host inxovh-hy003
 2   hdd 1.00000         osd.2             up  1.00000 1.00000
 3   hdd 1.00000         osd.3             up  1.00000 1.00000

– OK on les marque down:

root@inxovh-hy001 (node1):~# ceph osd down osd.0
marked down osd.0.
root@inxovh-hy001 (node1):~# ceph osd down osd.1
marked down osd.1.
root@inxovh-hy001 (node1):~# ceph osd down osd.2
marked down osd.2.
root@inxovh-hy001 (node1):~# ceph osd down osd.3
marked down osd.3.

– c’est mieux au niveau osd tree

root@inxovh-hy001 (node1):~# ceph osd tree
ID CLASS WEIGHT  TYPE NAME             STATUS REWEIGHT PRI-AFF
-1       6.00000 root default
-2       2.00000     host inxovh-hy001
 4   hdd 1.00000         osd.4             up  1.00000 1.00000
 5   hdd 1.00000         osd.5             up  1.00000 1.00000
-3       2.00000     host inxovh-hy002
 0   hdd 1.00000         osd.0           down  1.00000 1.00000
 1   hdd 1.00000         osd.1           down  1.00000 1.00000
-4       2.00000     host inxovh-hy003
 2   hdd 1.00000         osd.2           down  1.00000 1.00000
 3   hdd 1.00000         osd.3           down  1.00000 1.00000

– Mais le cluster (ce qui en reste) est toujours pas utilisable

root@inxovh-hy001 (node1):~# ceph -s
  cluster:
    id:     0054ab1b-7efb-4456-8e29-db1213048fd6
    health: HEALTH_WARN
            noout flag(s) set
            4 osds down
            2 hosts (4 osds) down
            Reduced data availability: 109 pgs inactive, 200 pgs down
            Degraded data redundancy: 200 pgs unclean

  services:
    mon: 1 daemons, quorum inxovh-hy001
    mgr: inxovh-hy001(active)
    osd: 6 osds: 2 up, 6 in
         flags noout

  data:
    pools:   3 pools, 200 pgs
    objects: 168k objects, 674 GB
    usage:   675 GB used, 1292 GB / 1968 GB avail
    pgs:     100.000% pgs not active
             200 down

  io:
    client:   289 GB/s rd, 1120 GB/s wr, 70215 kop/s rd, 41429 kop/s wr
    recovery: 525 GB/s, 784 keys/s, 131 kobjects/s

– A ce moment, on se dit “est ce qu’on a bien un replica a 3 au fait ?”. Allez on vérifie

root@inxovh-hy001 (node1):~#  ceph osd dump | grep "^pool"
pool 0 'data' replicated size 3 min_size 2 crush_rule 0 object_hash rjenkins pg_num 64 pgp_num 64 last_change 39347 flags hashpspool stripe_width 0 application rbd
pool 6 'libvirt-pool' replicated size 3 min_size 2 crush_rule 0 object_hash rjenkins pg_num 128 pgp_num 128 last_change 39348 flags hashpspool stripe_width 0 application rbd
pool 7 '.rgw.root' replicated size 3 min_size 2 crush_rule 0 object_hash rjenkins pg_num 8 pgp_num 8 last_change 39344 owner 18446744073709551615 flags hashpspool stripe_width 0 application rgw

Ouf.

– En regardant le status détaillé, on voit que plein de pg sont dans un état paralysé

# ceph health detail

# ceph pg 6.3f query

J’ai plus le output mais en gros, ca nous dit qu’il faut passer les osd down en lost.

{ "state": "down+peering",
...
 "blocked": "peering is blocked due to down osds",
         "down_osds_we_would_probe": [
               1],
   "peering_blocked_by": [
               { "osd": 1,
                 "current_lost_at": 0,
                 "comment": "starting or marking this osd lost may let us proceed"}]},

– On passe les OSD down en LOST

# ceph osd lost osd.0
Error EPERM: are you SURE?  this might mean real, permanent data loss.  pass–yes-i-really-mean-it if you really do.

– Pas très rassurant, mais on y va en se disant qu’il y aura pas de retour arrière possible (appeler OVH et leur demander de rallumer les serveurs en payant 1 mois supplémentaire).

 root@inxovh-hy001 (node1):/var/log/ceph# ceph osd lost osd.0 --yes-i-really-mean-it
marked osd lost in epoch 39718
 root@inxovh-hy001 (node1):/var/log/ceph# ceph osd lost osd.1 --yes-i-really-mean-it
marked osd lost in epoch 39718
 root@inxovh-hy001 (node1):/var/log/ceph# ceph osd lost osd.2 --yes-i-really-mean-it
marked osd lost in epoch 39718
 root@inxovh-hy001 (node1):/var/log/ceph# ceph osd lost osd.3 --yes-i-really-mean-it
marked osd lost in epoch 39718

– A ce moment, ca marche toujours pas, l’erreur est que la condition min_size n’est pas honoré et en effet, elle est à 2 alors que nous avons 1 seul serveur. On le met à 1:

root@inxovh-hy001 (node1):/var/log/ceph# ceph osd pool set libvirt-pool min_size 1

– Hourra, tout remarche

– On exporte la VM qui restait

root@inxovh-hy001 (node1):~# rbd export  libvirt-pool/dnxovh-web001_data /zdata/vms/dnxovh-web001_data.img
Exporting image: 100% complete...done.

We are happy.

Ubuntu et kernel Generic chez OVH ?

Pour des raisons technique, il m’a fallu utiliser des ubuntu sur des serveurs de la gamme HOSTING d’0VH.
Le Hic c’est que par default; c’est un kernel OVH et entre nous, je préfère une installation sur un noyau générique 🙂

La procédure qui suit fonctionne pour Ubuntu 14.04, Ubuntu 15.04 et ubuntu 15.15 (j’ai testé les 3).

Pour installer le kernel generic de Linux; c’était la galère notamment avec les pilotes de la carte réseau INTEL 10 Gigabit ixgbe.

Voici la démarche:

  • Installer le kernel générique
    apt-get install -y linux-image-generic linux-headers-generic
    
  • Déplacer le positionnement du noyau OVH en dernière position pour que le nouveau générique soit en premier au boot
    mv /etc/grub.d/06_OVHkernel /etc/grub.d/25_OVHkernel
    update-grub
    

    NE PAS REBOOTER MAINTENANT SINON FAUDRA PASSER EN MODE CONSOLE

  • Installer les pilotes de la carte réseau ixgbe (4.3.13 au moment de ce post) linké sur les nouveaux headers du kernel generic fraichement installé
    apt-get install -y make gcc
    BUILD_KERNEL=$(sed -n -e '/vmlinuz-.*-generic/p' /boot/grub/grub.cfg | sed -e 's/.*vmlinuz-\(.*-generic\) .*/\1/' | head -n 1)
    cd /usr/local/src
    wget https://downloadmirror.intel.com/14687/eng/ixgbe-4.3.13.tar.gz
    tar xzvf ixgbe-4.3.13.tar.gz
    cd ixgbe-4.3.13/src
    make CFLAGS_EXTRA="-DIXGBE_NO_LRO" BUILD_KERNEL="$BUILD_KERNEL" install
    
  • ne pas oublier l’update-initramfs sinon au prochain boot faudra faire un rmmod puis modprobe; pas très glorieux
    update-initramfs -tuk $BUILD_KERNEL
    

    Voila on peut maintenant rebooter; on sera sur un nouveau noyau GENERIC et la carte réseau fonctionnera !

    # uname -a
    Linux dnxovh-hy001 3.13.0-77-generic #121-Ubuntu SMP Wed Jan 20 10:50:42 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
    # lsmod |grep ix
    ixgbe                 293872  0
    vxlan                  37619  1 ixgbe
    dca                    15130  1 ixgbe
    ptp                    18933  1 ixgbe
    
  • bhyve – Guest openbsd

    Bhyve – Guest openbsd

  • start_openbsd.sh
    
    #!/bin/sh
     
    grub-bhyve -m obsd57.map -r hd0,msdos1 -M 1024M openbsd < obsd57.in
     
    bhyve -w -AI -H -P -s 0:0,hostbridge  \
    -s 1:0,lpc -s 2:0,virtio-net,tap2 \
    -s 3:0,virtio-blk,./disk.img \
    -l com1,stdio -c 2 \
    -m 1024M openbsd
    
  • obsd57.in
    kopenbsd -h com0 -r sd0a (hd0,openbsd1)/bsd
    boot
    
  • obsd57.map
    (hd0) ./disk.img
    (cd0) ./install57.iso
    
  • halt.sh
     
    #!/bin/sh
    bhyvectl --destroy --vm=openbsd
    
  • FreeBSD – bhyve et libvirt , le combo parfait

    FreeBSD avec bhyve et libvirt

  • Tout d’abord; il faut installer des packages
    pkg install -y libvirt
    
  • Le configurer au démarrage dans /etc/rc.conf
    libvirtd_enable="YES"
    
  • Modifier /etc/loader.conf pour y ajouter les 2 kernel modules suivant
    vmm_load="YES"
    nmdm_load="YES"
    
  • créer un fichier vide pour l’image de la VM
    truncate alcsys-vm51.img -s 100G
    
  • Télécharger l’iso de la disto souhaitée
    wget http://xxxxx/CentOS-7-x86_64-Minimal-1511.iso
    
  • Préparer un fichier device.map
    # cat device.map
    (hd0) ./alcsys-vm51.img
    (cd0) ./CentOS-7-x86_64-Minimal-1511.iso
    
  • préparer grub
    grub-bhyve -m device.map -r cd0 -M 2048 alcsys-vm51
    
  • Puis taper dans le prompt grub>
     linux (cd0)/isolinux/vmlinuz
     initrd (cd0)/isolinux/initrd.img
     boot
    
  • Si ca marche pas; taper “ls (cd0)/” pour localiser vmlinuz sur l’iso
  • Créer une interface réseau pour la VM
    ifconfig tap1 create
    
  • Démarrer la VM
    bhyve -AI -H -P -s 0:0,hostbridge -s 1:0,lpc -s 2:0,virtio-net,tap1 -s 3:0,virtio-blk,./alcsys-vm51.img -s 4:0,ahci-cd,./CentOS-7-x86_64-Minimal-1511.iso -l com1,stdio -c 4 -m 2048M alcsys-vm51
    
  • l’installation de l’os débute; effectuez l’installation en mode non graphique
  • Une fois terminée, détruisez le domain
    bhyvectl --destroy --vm=alcsys-vm51
    
  • Préparer de nouveau grub
    grub-bhyve -m device.map -r hd0,msdos1 -M 2048M alcsys-vm51
    
  • dans le prompt grub> taper:
    configfile (hd0,msdos1)/grub2/grub.cfg
    
  • Démarrer la VM
    bhyve -AI -H -P -s 0:0,hostbridge -s 1:0,lpc -s 2:0,virtio-net,bridge0 -s 3:0,virtio-blk,./alcsys-vm51.img -l com1,stdio -c 4 -m 2048M alcsys-vm51
    
  • Vérifier que l’os est ok
  • halt de la vm et tester de passer les instructions comme ceci [A VALIDER]:
    echo "configfile (hd0,msdos1)/grub2/grub.cfg" | grub-bhyve -m device.map -r hd0,msdos1 -M 2048M alcsys-vm51
    bhyve -AI -H -P -s 0:0,hostbridge -s 1:0,lpc -s 2:0,virtio-net,bridge0 -s 3:0,virtio-blk,./alcsys-vm51.img -l com1,stdio -c 4 -m 2048M alcsys-vm51
    
  • Créer un XML pour libvirt
    
    
    
    <domain type='bhyve' id='97406'>
      <name>alcsys-vm51</name>
      <uuid>df3be7e7-a104-11e3-aeb0-50e5492bd3dc</uuid>
      <memory unit='KiB'>2097152</memory>
      <currentMemory unit='KiB'>2097152</currentMemory>
      <vcpu placement='static'>2</vcpu>
      <bootloader>/usr/local/sbin/grub-bhyve</bootloader>
      <bootloader_args>--directory=/grub2 --grub-cfg=grub.cfg -r hd0,msdos1 -m /vm/alcsys-vm51.map -M 2048 alcsys-vm51</bootloader_args>
      <os>
        <type arch='x86_64'>hvm</type>
      </os>
      <features>
        <acpi/>
        <apic/>
      </features>
      <clock offset='utc'/>
      <on_poweroff>destroy</on_poweroff>
      <on_reboot>restart</on_reboot>
      <on_crash>destroy</on_crash>
      <devices>
        <disk type='file' device='disk'>
          <driver name='file' type='raw'/>
          <source file='/vm/alcsys-vm51.img'/>
          <backingStore/>
          <target dev='vdb' bus='virtio'/>
          <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
        </disk>
        <controller type='pci' index='0' model='pci-root'/>
        <controller type='sata' index='0'/>
        <interface type='bridge'>
          <mac address='52:54:00:c6:2d:7c'/>
          <source bridge='bridge0'/>
          <target dev='vnet1'/>
          <model type='virtio'/>
          <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
        </interface>
        <serial type='nmdm'>
          <source master='/dev/nmdm0A' slave='/dev/nmdm0B'/>
          <target port='0'/>
        </serial>
        <console type='nmdm'>
          <source master='/dev/nmdm0A' slave='/dev/nmdm0B'/>
          <target type='serial' port='0'/>
        </console>
      </devices>
    </domain>
    
  • le fichier map n’a plus besoin du cdrom et bien s’assurer du path absolu de l’image
    # cat /vm/alcsys-vm51.map
    (hd0) /vm/alcsys-vm51.img
    
  • importer le xml
    virsh define alcsys-vm51.xml
    
  • démarrer le domain
    virsh start alcsys-vm51
    
  • Vérifier
    # virsh list
     Id    Name                           State
    ----------------------------------------------------
     37911 alcsys-vm51                    running
    
  • Accéder a la console
    virsh console alcsys-vm51 
    
  • Le log a consulter
    tail -f /var/log/libvirt/bhyve/alcsys-vm51.log
    
  • FreeBSD / Packet Filter – Rediriger le traffic http vers un serveur ayant 2 NIC

    Un petit problème s’est posé lors d’un besoin très spécifique consistant à rediriger temporairement le traffic http d’une jail vers une autre jail hébergée sur un autre serveur ayant sa propre GW.

    En image, cela sera plus parlant:

    PF RDR issue with 2 NIC - New Page

    Comme on peut le voir; avec une simple règle RDR, le traffic retour repartait sur la gw du Server B (WAN) au lieu de revenir par son chemin original (LAN).

    La solution est d’ajouter sur le ServerA une règle PF indiquant de réutiliser une interface particulière – en l’occurence la même que celle utilisée pour l’allée – pour le chemin du retour du paquet.

    La règle:

    pass in quick on vlan10 reply-to (vlan10 192.168.10.6) proto tcp from any to 192.168.10.206 port http keep state label pra.nli.http
    

    Maintenant le paquet emprunte la même interface pour l’allée et le retour.

    letsencrypt, le SSL gratuit pour tous !

    Ayant testé la beta, je suis désormais fan de ce service.

    Voici comment l’utiliser sous Freebsd:

    * Récupérer les sources

    git clone https://github.com/letsencrypt/letsencrypt.git
    

    * Installer les dépendances

     ./letsencrypt-auto --help --debug
    

    * Créer un fichier common.ini

    mkdir /usr/local/www/letsencrypt
    mkdir /usr/local/etc/letsencrypt
    vi mkdir /usr/local/etc/letsencrypt/common.ini
    
    authenticator = webroot
    webroot-path = /usr/local/www/letsencrypt
    server = https://acme-v01.api.letsencrypt.org/directory
    renew-by-default
    agree-tos
    email = xxxx@domain.com
    rsa-key-size = 4096
    

    * Editer la conf apache sur son vhost http/80

    
            ##################
            # LETS ENCRYPT
            ##################
        Alias "/.well-known/acme-challenge" "/usr/local/www/letsencrypt/.well-known/acme-challenge"
        
            Header set Content-Type "application/jose+json"
        
    
    

    * Si vous avez une rewriteRule qui redirige le http => https; il faut ajouter une condition d’exclusion:

            RewriteEngine On
            RewriteCond %{REMOTE_PORT}  !^443$
            RewriteCond %{REQUEST_URI} !^/\.well\-known/acme\-challenge/
            RewriteRule ^/(.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
    

    * Générer son certif

    root@j-transmission:~/letsencrypt # ./letsencrypt-auto --server https://acme-v01.api.letsencrypt.org/directory certonly -c /usr/local/etc/letsencrypt/common.ini -d blog.distran.org
    Updating letsencrypt and virtual environment dependencies.......
    Running with virtualenv: /root/.local/share/letsencrypt/bin/letsencrypt --server https://acme-v01.api.letsencrypt.org/directory certonly -c /usr/local/etc/letsencrypt/common.ini -d blog.distran.org
    
    IMPORTANT NOTES:
     - If you lose your account credentials, you can recover through
       e-mails sent to letsencrypt@distran.org.
     - Congratulations! Your certificate and chain have been saved at
       /etc/letsencrypt/live/blog.distran.org/fullchain.pem. Your cert
       will expire on 2016-03-11. To obtain a new version of the
       certificate in the future, simply run Let's Encrypt again.
     - Your account credentials have been saved in your Let's Encrypt
       configuration directory at /etc/letsencrypt. You should make a
       secure backup of this folder now. This configuration directory will
       also contain certificates and private keys obtained by Let's
       Encrypt so making regular backups of this folder is ideal.
     - If like Let's Encrypt, please consider supporting our work by:
    
       Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
       Donating to EFF:                    https://eff.org/donate-le
    
    

    * Editer la conf apache pour que ca pointe sur le certif

    SSLCertificateFile "/etc/letsencrypt/live/blog.distran.org/fullchain.pem"
    SSLCertificateKeyFile "/etc/letsencrypt/live/blog.distran.org/privkey.pem"
    

    * EN crontab pour une execution tous les 31 de chaque mois; donc 1 fois tous les 2 mois sachant que ca expire au bout de 3 mois:

    10 3 31 * *   ( /root/letsencrypt/letsencrypt-auto --server https://acme-v01.api.letsencrypt.org/directory certonly -c /usr/local/etc/letsencrypt/commo
    n.ini -d seed2.distran.org && /usr/local/etc/rc.d/apache24 restart ) > /tmp/cron_letsencrypt.log 2>&1
    

    Setup d’un Annuaire LDAP s’appuyant sur le backend SQL de postfixadmin

    L’idée est d’avoir une base source unique pour ses utilisateurs.
    Postfixadmin s’appuye sur une db sql et ne permet pas l’usage d’un annuaire LDAP en backend. Pourtant beaucoup d’applications s’appuyent sur du LDAP, c’est donc handicapant de n’avoir qu’une base sql.
    Alors voici un moyen d’avoir également un annuaire LDAP automatiquement mappé sur la base sql utilisateurs de postfixadmin.

    Ce setup s’appuye sur des HASH SHA512 pour le stockage des passwords.

    * /usr/local/www/postfixadmin/config.inc.php

    Par default, postfixadmin créé des hash MD5 pour stocker les password, c’est pas très rassurant, alors mettons nous plutot sur du SHA512, réputé plus solide.

    / Encrypt
    // In what way do you want the passwords to be crypted?
    // md5crypt = internal postfix admin md5
    // md5 = md5 sum of the password
    // system = whatever you have set as your PHP system default
    // cleartext = clear text passwords (ouch!)
    // mysql_encrypt = useful for PAM integration
    // authlib = support for courier-authlib style passwords
    // dovecot:CRYPT-METHOD = use dovecotpw -s 'CRYPT-METHOD'. Example: dovecot:CRAM-MD5
    $CONF['encrypt'] = 'dovecot:SHA512-CRYPT';
    

    * Cette étape n’a d’importance uniquement si vous souhaitez faire du PAM LDAP sur vos OS.

    – Ajouter une colonne id sur le schéma existant de postfixadmin de la table mailbox:
    Et Pas d’inquiétude; ca ne casse rien au bon fonctionnement de postfixadmin

    ALTER TABLE mailbox ADD id INT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE KEY;
    

    – Alimenter cette colonne avec des id sur les entrées existantes; à partir de 1100 pour ne pas empiéter sur des services utilisant les ranges inférieures.

    – Installer l’annuaire Openldap

    # cd /usr/ports/net/openldap24-server
    # make config
    
    Cocher ODBC
    Cocher SHA2
    
    # make install clean
    

    – Editer /usr/local/etc/openldap/slapd.conf

    
    # See slapd.conf(5) for details on configuration options.
    # This file should NOT be world readable.
    #
    include         /usr/local/etc/openldap/schema/core.schema
    include         /usr/local/etc/openldap/schema/cosine.schema
    include         /usr/local/etc/openldap/schema/inetorgperson.schema
    include         /usr/local/etc/openldap/schema/nis.schema
    
    
    TLSCipherSuite HIGH:MEDIUM:+SSLv3
    TLSCertificateFile /usr/local/etc/openldap/cert.crt
    TLSCertificateKeyFile /usr/local/etc/openldap/cert.key
    
    
    pidfile         /var/run/openldap/slapd.pid
    argsfile        /var/run/openldap/slapd.args
    
    # Load dynamic backend modules:
    modulepath      /usr/local/libexec/openldap
    moduleload      back_sql
    
    # Module for supporting SHA512
    moduleload      pw-sha2
    
    #######################################################################
    # sql database definitions
    #######################################################################
    
    database        sql
    suffix          "dc=infranix,dc=eu"
    # You only need these if normal ldap backends are defined and hold the "root"
    rootdn          "cn=admin,dc=infranix,dc=eu"
    
    # Postfixadmin Password format (SHA512) using libcrypt)
    password-hash {CRYPT}
    password-crypt-salt-format "$6$%.16s"
    
    dbhost          192.168.X.Y
    dbname          ldap
    dbuser          ldap_user
    dbpasswd        xxxxxxxxxx
    
    lastmod off
    
    subtree_cond    "ldap_entries.dn LIKE CONCAT('%',?)"
    has_ldapinfo_dn_ru      no
    

    * Créer une db ldap

    create database ldap;
    

    La suite du schéma de la base va avoir beaucoup de vues afin d’avoir une automatisation extrême.

    * Pour privilégier un domain le schéma aura 2 vues users quasi-identiques, l’une incluant le domaine privilégié et l’autre l’excluant.

    * Vue users_publics

    drop view users_public;
    
    create view users_public AS
    
    select 
      id as id, 
      local_part as username ,
     password,
      name,
      name as surname,
      username as email,
      domain as domain
    from postfix.mailbox where mailbox.active =1 and mailbox.username  NOT LIKE '%@infranix.eu';
    

    – Vue users_people

    drop view users_people;
    
    create view users_people AS
    
    select 
      id as id, 
      local_part as username ,
     password,
      name,
      name as surname,
      username as email,
      domain as domain
    from postfix.mailbox where mailbox.active =1 and mailbox.username  LIKE '%@infranix.eu';
    
    

    – users

    drop view users
    
    create view users AS
    
    select * from users_people
    UNION
    select * from users_public
    

    – org_unit_original

    c’est à la base ce schéma qui est fourni par default.

    CREATE TABLE `org_unit_original` (
      `id` int(11) NOT NULL,
      `o` varchar(32) DEFAULT NULL,
      `dn` varchar(255) DEFAULT NULL,
      `parent` int(11) DEFAULT NULL,
      `oc_map_id` int(2) DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `dn` (`dn`)
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
    INSERT INTO `org_unit_original` VALUES (1,'Infranix','dc=infranix,dc=eu',0,3),(2,'People','ou=People,dc=infranix,dc=eu',1,2),(3,'Groups','ou=Groups,dc=infranix,dc=eu',1,2),(4,'Public','ou=Public,dc=infranix,dc,eu',1,2),(6,'Devnix','dc=devnix.fr,dc=infranix,dc=eu',1,3),(7,'People','ou=People,dc=devnix.fr,dc=infranix,dc=eu',6,2);
    
    
    

    mysql> select id,o,dn,parent,oc_map_id from org_unit_original;
    +—-+———-+——————————————+——–+———–+
    | id | o | dn | parent | oc_map_id |
    +—-+———-+——————————————+——–+———–+
    | 1 | Infranix | dc=infranix,dc=eu | 0 | 3 |
    | 2 | People | ou=People,dc=infranix,dc=eu | 1 | 2 |
    | 3 | Groups | ou=Groups,dc=infranix,dc=eu | 1 | 2 |
    | 4 | Public | ou=Public,dc=infranix,dc,eu | 1 | 2 |
    | 6 | Devnix | dc=devnix.fr,dc=infranix,dc=eu | 1 | 3 |
    | 7 | People | ou=People,dc=devnix.fr,dc=infranix,dc=eu | 6 | 2 |
    +—-+———-+——————————————+——–+———–+

    – org_unit_distributed

    On va créé une orgUnit pour chaque domaine via une vue s’appuyant sur la vue users_distributed, elle même une vue générée par la table mailbox de postfixadmin.

    drop view org_unit_distributed;
    create view org_unit_distributed AS
    
    
       select 
        (domain) as o,
       MIN((id)+50000) as id,
       CONCAT('dc=',domain,',dc=infranix,dc=eu') AS dn, 
            3 as oc_map_id,
            1 as parent
       from users_distributed group by domain
     
    UNION 
    
    select * from org_unit_original;
    
    

    * org_unit

    Cette vue définit l’arbre du LDAP avec la relation entre niveau (parent).

    drop view org_unit;
    create view org_unit AS
    
    select id,o,dn,parent,oc_map_id from org_unit_original
    
    UNION
    
       select 
       
       MIN((id)+50000) as id,
        (domain) as o,
       CONCAT('dc=',domain,',dc=infranix,dc=eu') AS dn, 
    
            1 as parent,
                    3 as oc_map_id
       from users_distributed group by domain
     
    UNION 
    
    
        SELECT
         (users_distributed.id)+90000 as id,
            'People' ,
            CONCAT('ou=People,dc=',domain,',dc=infranix,dc=eu') AS dn, 
      
            org_unit_distributed.id as parent,
                    2 as oc_map_id
            from users_distributed, org_unit_distributed
            where org_unit_distributed.o = domain
            group by domain
    
    mysql> select * from org_unit limit 10;
    +--------+-------------------+------------------------------------------+--------+-----------+
    | id     | o                 | dn                                       | parent | oc_map_id |
    +--------+-------------------+------------------------------------------+--------+-----------+
    |     1  | Infranix          | dc=infranix,dc=eu                        |      0 |         3 |
    |     2  | People            | ou=People,dc=infranix,dc=eu              |      1 |         2 |
    |     3  | Groups            | ou=Groups,dc=infranix,dc=eu              |      1 |         2 |
    |     4  | Public            | ou=Public,dc=infranix,dc,eu              |      1 |         2 |
    |     6  | Devnix            | dc=devnix.fr,dc=infranix,dc=eu           |      1 |         3 |
    |     7  | People            | ou=People,dc=devnix.fr,dc=infranix,dc=eu |      6 |         2 |
    | 60047  | toto.net          | dc=toto.net,dc=infranix,dc=eu            |      1 |         3 |
    | 60045  | titi.com          | dc=titi.com,dc=infranix,dc=eu            |      1 |         3 |
    | 60009  | tutu.biz          | dc=tutu.biz,dc=infranix,dc=eu            |      1 |         3 |
    | 60021  | aaaa.com          | dc=aaaa.com,dc=infranix,dc=eu            |      1 |         3 |
    | 100044 | People            | ou=People,dc=toto.net,dc=infranix,dc=eu  |  60044 |         2 |
    | 100000 | People            | ou=People,dc=titi.com,dc=infranix,dc=eu  |  60000 |         2 |
    | 100007 | People            | ou=People,dc=tutu.biz,dc=infranix,dc=eu  |  60007 |         2 |
    | 100014 | People            | ou=People,dc=aaaaa.com,dc=infranix,dc=eu |  60014 |         2 |
    +--------+-------------------+------------------------------------------+--------+-----------+
    10 rows in set (0.01 sec)
    

    * ldap_entries

    drop view ldap_entries;
    
    
    CREATE VIEW ldap_entries AS 
     
    SELECT 
        org_unit.id AS id,
        org_unit.dn AS dn,
        org_unit.oc_map_id AS oc_map_id,
        org_unit.parent AS parent,
        org_unit.id AS keyval
    FROM
        org_unit
     
    
    UNION 
    
     
    SELECT 
        (users_people.id) AS id,
        CONCAT('uid=',
                        users_people.username,
                        ',ou=People,dc=infranix,dc=eu') AS dn,
        1 AS oc_map_id,
        2 AS parent,
        (users_people.id) AS keyval
    FROM
        users_people
    
    
    
     
    UNION 
     
    SELECT 
     
        (groups.id) AS id,
        CONCAT( 'cn=',
                        groups.name,
                        ',ou=Groups,dc=infranix,dc=eu') AS dn,
        5 AS oc_map_id,
        3 AS parent,
        groups.id AS id
    FROM
        groups
    
    
    UNION
    
    SELECT 
        (users_distributed.id) AS id,
        CONCAT('uid=',
                users_distributed.username,
                ',ou=People,dc=',
                users_distributed.domain,
                ',dc=infranix,dc=eu') AS dn,
        1 AS oc_map_id,
        org_unit.id AS parent,
        (users_distributed.id) AS keyval
    FROM
        users_distributed,org_unit
    WHERE
        org_unit.dn = CONCAT('ou=People,dc=',users_distributed.domain,',dc=infranix,dc=eu');
    
    

    * ldap_entry_objclasses

    Pour l’usage de pam, il fallait ajouter de nouveaux objectClass à inetOrgPerson, notamment posixAccount et shadowAccount.
    L’astuce est de créer une vue pour construire la table optionnelle ldap_entry_objclasses en l’autogénérant via la table users.

    drop view ldap_entry_objclasses;
    
    CREATE VIEW ldap_entry_objclasses AS 
     
    SELECT 
        (users.id) AS entry_id,
        'posixAccount' AS oc_name
    FROM
        users
     
    UNION 
    
    SELECT 
        (users.id) AS entry_id,
        'shadowAccount' AS oc_name
    FROM
        users
    

    * ldap_oc_mappings

    Cette table permet d’identifier la table de toutes les objectCLass

    CREATE TABLE `ldap_oc_mappings` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `name` varchar(64) NOT NULL,
      `keytbl` varchar(64) NOT NULL,
      `keycol` varchar(64) NOT NULL,
      `create_proc` varchar(255) DEFAULT NULL,
      `delete_proc` varchar(255) DEFAULT NULL,
      `expect_return` tinyint(4) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=MyISAM AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
    
    INSERT INTO `ldap_oc_mappings` VALUES (3,'organization','org_unit','id',NULL,NULL,0),(2,'organizationalUnit','org_unit','id',NULL,NULL,0),(1,'inetOrgPerson','users','id',NULL,NULL,0),(5,'posixGroup','groups','id',NULL,NULL,0),(6,'posixAccount','users','id',NULL,NULL,0);
    
    

    son contenu:

    mysql> select * from ldap_oc_mappings;
    +----+--------------------+----------+--------+-------------+-------------+---------------+
    | id | name               | keytbl   | keycol | create_proc | delete_proc | expect_return |
    +----+--------------------+----------+--------+-------------+-------------+---------------+
    |  3 | organization       | org_unit | id     | NULL        | NULL        |             0 |
    |  2 | organizationalUnit | org_unit | id     | NULL        | NULL        |             0 |
    |  1 | inetOrgPerson      | users    | id     | NULL        | NULL        |             0 |
    |  5 | posixGroup         | groups   | id     | NULL        | NULL        |             0 |
    |  6 | posixAccount       | users    | id     | NULL        | NULL        |             0 |
    +----+--------------------+----------+--------+-------------+-------------+---------------+
    

    * ldap_attr_mappings

    Cette table est cruciale; c’est elle qui définit les attributs de chaque objectClass.
    Dans mon usage pour du pam_ldap, il fallait donc bien définir homeDirectory, userPassword, uidMember, gidNumber, loginShell:

    CREATE TABLE `ldap_attr_mappings` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `oc_map_id` int(10) unsigned NOT NULL,
      `name` varchar(255) NOT NULL,
      `sel_expr` varchar(255) NOT NULL,
      `sel_expr_u` varchar(255) DEFAULT NULL,
      `from_tbls` varchar(255) NOT NULL,
      `join_where` varchar(255) DEFAULT NULL,
      `add_proc` varchar(255) DEFAULT NULL,
      `delete_proc` varchar(255) DEFAULT NULL,
      `param_order` tinyint(4) NOT NULL,
      `expect_return` tinyint(4) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=MyISAM AUTO_INCREMENT=23 DEFAULT CHARSET=utf8;
    
    INSERT INTO `ldap_attr_mappings` VALUES (1,1,'cn','users.username','','users',NULL,NULL,NULL,3,0),(2,1,'telephoneNumber','users.phone','','users','NULL',NULL,NULL,3,0),(3,1,'sn','users.name','','users',NULL,NULL,NULL,3,0),(7,1,'homeDirectory','concat(\'/home/\',users.domain, \'/\',users.username)',NULL,'users',NULL,NULL,NULL,3,0),(5,1,'mail','users.email',NULL,'users',NULL,NULL,NULL,3,0),(6,2,'ou','Groups',NULL,'groups',NULL,NULL,NULL,3,0),(13,1,'userPassword','replace(users.password, \'SHA512-CRYPT\', \'CRYPT\')',NULL,'users','users.password IS NOT NULL',NULL,NULL,3,0),(8,3,'o','org_unit.o',NULL,'org_unit',NULL,NULL,NULL,3,0),(14,1,'displayName','users.name',NULL,'users',NULL,NULL,NULL,3,0),(9,1,'loginShell','concat(\'/bin/bash\')',NULL,'users',NULL,NULL,NULL,3,0),(4,1,'uidNumber','users.id',NULL,'users',NULL,NULL,NULL,3,0),(21,5,'cn','groups.name',NULL,'groups',NULL,NULL,NULL,3,0),(15,5,'gidNumber','groups.id',NULL,'groups',NULL,NULL,NULL,3,0),(16,5,'memberUid','users.username',NULL,'groups,group_member,users','group_member.users_id=users.id and group_member.groups_id=groups.id',NULL,NULL,3,0),(11,1,'gidNumber','100',NULL,'users',NULL,NULL,NULL,3,0),(17,3,'objectClass','concat(\'top\')',NULL,'org_unit',NULL,NULL,NULL,3,0),(18,3,'objectClass','concat(\'dcObject\')',NULL,'org_unit',NULL,NULL,NULL,3,0),(19,1,'uid','users.username',NULL,'users',NULL,NULL,NULL,3,0),(20,3,'dc','org_unit.o',NULL,'org_unit',NULL,NULL,NULL,3,0);
    
    

    Le contenu des attributs de l’objectClass inetOrgPerson (id àa 1 dans la table ldap_oc_mappings ci-dessus) via un SELECT :

    
    mysql> mysql> select * from ldap_attr_mappings where oc_map_id=1;
    +----+-----------+-----------------+---------------------------------------------------+------------+-----------+----------------------------+----------+-------------+-------------+---------------+
    | id | oc_map_id | name            | sel_expr                                          | sel_expr_u | from_tbls | join_where                 | add_proc | delete_proc | param_order | expect_return |
    +----+-----------+-----------------+---------------------------------------------------+------------+-----------+----------------------------+----------+-------------+-------------+---------------+
    |  1 |         1 | cn              | users.username                                    |            | users     | NULL                       | NULL     | NULL        |           3 |             0 |
    |  2 |         1 | telephoneNumber | users.phone                                       |            | users     | NULL                       | NULL     | NULL        |           3 |             0 |
    |  3 |         1 | sn              | users.name                                        |            | users     | NULL                       | NULL     | NULL        |           3 |             0 |
    |  7 |         1 | homeDirectory   | concat('/home/',users.domain, '/',users.username) | NULL       | users     | NULL                       | NULL     | NULL        |           3 |             0 |
    |  5 |         1 | mail            | users.email                                       | NULL       | users     | NULL                       | NULL     | NULL        |           3 |             0 |
    | 13 |         1 | userPassword    | replace(users.password, 'SHA512-CRYPT', 'CRYPT')  | NULL       | users     | users.password IS NOT NULL | NULL     | NULL        |           3 |             0 |
    | 14 |         1 | displayName     | users.name                                        | NULL       | users     | NULL                       | NULL     | NULL        |           3 |             0 |
    |  9 |         1 | loginShell      | concat('/bin/bash')                               | NULL       | users     | NULL                       | NULL     | NULL        |           3 |             0 |
    |  4 |         1 | uidNumber       | users.id                                          | NULL       | users     | NULL                       | NULL     | NULL        |           3 |             0 |
    | 11 |         1 | gidNumber       | 100                                               | NULL       | users     | NULL                       | NULL     | NULL        |           3 |             0 |
    | 19 |         1 | uid             | users.username                                    | NULL       | users     | NULL                       | NULL     | NULL        |           3 |             0 |
    +----+-----------+-----------------+---------------------------------------------------+------------+-----------+----------------------------+----------+-------------+-------------+---------------+
    
    

    A suivre, je vais alimenter au fur et à mesure.

    FreeBSD 10.2 – pam_ldap howto

    – Il faut installer les packages:

    # pkg install -y nss_ldap pam_ldap openldap-client p5-Mozilla-CA
    

    – Faire la conf du client ldap

    vi /usr/local/etc/nss_ldap.conf
    
    BASE ou=People,dc=infranix,dc=eu
    URI ldaps://ldap1
    TLS_CACERTDIR /usr/local/lib/perl5/site_perl/Mozilla/CA/cacert.pem
    TLS_REQCERT allow
    SASL_NOCANON    on
    

    – Créer des symlink pour simplier la configuration vers ldap.conf

    # ln -s /usr/local/etc/nss_ldap.conf /usr/local/etc/openldap/ldap.conf
    # ln -s /usr/local/etc/nss_ldap.conf /usr/local/etc/ldap.conf
    

    – Modifier nsswitch pour qu’il s’appuye sur ldap pour passwd et group

    [root@app001 ~]# cat /etc/nsswitch.conf
    #
    # nsswitch.conf(5) - name service switch configuration file
    # $FreeBSD: releng/10.2/etc/nsswitch.conf 224765 2011-08-10 20:52:02Z dougb $
    #
    group: files ldap
    group_compat: nis
    hosts: files dns
    networks: files
    passwd: files ldap
    passwd_compat: nis
    shells: files
    services: compat
    services_compat: nis
    protocols: files
    rpc: files
    

    – Modifier /etc/pam.d/ssh

    root@app001 ~]# cat /etc/pam.d/sshd
    #
    # $FreeBSD: releng/10.2/etc/pam.d/sshd 197769 2009-10-05 09:28:54Z des $
    #
    # PAM configuration for the "sshd" service
    #
    
    # auth
    auth        sufficient  /usr/local/lib/pam_ldap.so    no_warn try_first_pass
    auth		sufficient	pam_opie.so		no_warn no_fake_prompts
    auth		requisite	pam_opieaccess.so	no_warn allow_local
    #auth		sufficient	pam_krb5.so		no_warn try_first_pass
    #auth		sufficient	pam_ssh.so		no_warn try_first_pass
    auth		required	pam_unix.so		no_warn try_first_pass
    
    # account
    account     required    /usr/local/lib/pam_ldap.so    no_warn ignore_authinfo_unavail ignore_unknown_user
    account		required	pam_nologin.so
    #account	required	pam_krb5.so
    account		required	pam_login_access.so
    account		required	pam_unix.so
    
    # session
    #session	optional	pam_ssh.so		want_agent
    session     required    /usr/local/lib/pam_mkhomedir.so
    session		required	pam_permit.so
    
    # password
    #password	sufficient	pam_krb5.so		no_warn try_first_pass
    password	required	pam_unix.so		no_warn try_first_pass
    

    – modifier /etc/pam.d/su pour mettre le bon groupe

    [root@app001 ~]# cat /etc/pam.d/su
    #
    # $FreeBSD: releng/10.2/etc/pam.d/su 219663 2011-03-15 10:13:35Z des $
    #
    # PAM configuration for the "su" service
    #
    
    # auth
    auth		sufficient	pam_rootok.so		no_warn
    auth		sufficient	pam_self.so		no_warn
    auth		requisite	pam_group.so		no_warn group=wheel,users root_only fail_safe ruser
    auth		include		system
    
    # account
    account		include		system
    
    # session
    session		required	pam_permit.so
    

    – modifier /etc/pam.d/system

    root@app001 /etc/pam.d]# cat system

    # $FreeBSD: releng/10.2/etc/pam.d/system 197769 2009-10-05 09:28:54Z des $
    #
    # System-wide defaults
    #
    
    # auth
    auth		sufficient	/usr/local/lib/pam_ldap.so   no_warn try_first_pass
    auth		sufficient	pam_opie.so		no_warn no_fake_prompts
    auth		requisite	pam_opieaccess.so	no_warn allow_local
    #auth		sufficient	pam_krb5.so		no_warn try_first_pass
    #auth		sufficient	pam_ssh.so		no_warn try_first_pass
    auth		required	pam_unix.so		no_warn try_first_pass nullok
    
    # account
    #account	required	pam_krb5.so
    account         required        /usr/local/lib/pam_ldap.so   ignore_unknown_user ignore_authinfo_unavail
    account		required	pam_login_access.so
    account		required	pam_unix.so
    
    # session
    #session	optional	pam_ssh.so		want_agent
    session		required	/usr/local/lib/pam_mkhomedir.so
    session		required	pam_lastlog.so		no_fail
    
    # password
    #password	sufficient	pam_krb5.so		no_warn try_first_pass
    password	required	pam_unix.so		no_warn try_first_pass
    

    – modifier /etc/pam.d/passwd

    [root@app001 /etc/pam.d]# cat passwd
    #
    # $FreeBSD: releng/10.2/etc/pam.d/passwd 113967 2003-04-24 12:22:42Z des $
    #
    # PAM configuration for the "passwd" service
    #
    
    # passwd(1) does not use the auth, account or session services.
    
    # password
    #password	requisite	pam_passwdqc.so		enforce=users
    #password  required    pam_ldap.so
    password	required	pam_unix.so		no_warn try_first_pass nullok
    

    – OPTIONNEL : forcer le gidMember

    vi /etc/group
    users:*:100:
    

    – pour tester

    getent passwd
    

    Adapter LDAP pour supporter le SHA512 de postfixadmin

    Dans un prochain post; je présenterais comment créer un schéma LDAP automatiquement en s’appuyant sur un backend SQL; et en l’occurence, celui de postfixadmin.
    Toujours est-il que voici en avance une astuce pour qu’openldap reconnaisse les password SHA512 de postfixadmin.

    * slapd.conf

    Il faut avant tout activer le module sha2 qui permet de supporter des hash SHA étendu (256, 512):

    moduleload      pw-sha2
    

    Le format salt ici est ‘$6$’ qui invoque une méthode hash basée sur SHA512 et fournit 16 caractères de salt.

    password-hash {CRYPT}                                                                                                                                                  
    password-crypt-salt-format "$6$%.16s"
    

    * SQL

    update ldap_attr_mappings set sel_expr='replace(users.password, \'SHA512-CRYPT\', \'CRYPT\')' where id=13;
    
    MySQL [ldap]> select * from ldap_attr_mappings where id=13;
    +----+-----------+--------------+--------------------------------------------------+------------+-----------+----------------------------+----------+-------------+-----
    --------+---------------+
    | id | oc_map_id | name         | sel_expr                                         | sel_expr_u | from_tbls | join_where                 | add_proc | delete_proc | para
    m_order | expect_return |
    +----+-----------+--------------+--------------------------------------------------+------------+-----------+----------------------------+----------+-------------+-----
    --------+---------------+
    | 13 |         1 | userPassword | replace(users.password, 'SHA512-CRYPT', 'CRYPT') | NULL       | users     | users.password IS NOT NULL | NULL     | NULL        |
         3 |             0 |
    +----+-----------+--------------+--------------------------------------------------+------------+-----------+----------------------------+----------+-------------+-----
    --------+---------------+
    1 row in set (0.01 sec)