Tuesday, 02 April 2019
Écrit par
Grégory Soutadé

It's now one year I use Let's Encrypt TLS wildcard certificates. Until now, all was fine, but since the beginning of 2019, there is two domains on my certificate : and * and (maybe due to my certificate generation) I need to perform two challenges for renewal : HTTP (http01) and DNS (dns01).

So, I wrote a Python script that performs both :

#!/usr/bin/env python3
#-*- encoding: utf-8 -*-

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <>.

# Handle certificate renewal using HTTP and DNS challenges
# DNS challenge performed by Gandi Live v5 API

import requests
import os
import argparse
import shutil

# Config
ACME_RECORD = '_acme-challenge'
ACME_CHALLENGE_PATH = '/var/www/.well-known/acme-challenge'

headers = {
    'X-Api-Key': API_KEY,

CERTBOT_TOKEN = os.environ.get('CERTBOT_TOKEN', None)
DOMAIN = os.environ.get('CERTBOT_DOMAIN', None)

optparser = argparse.ArgumentParser(description='Letsencrypt challenge for Gandi v5 API')
optparser.add_argument('-c', '--cleanup', dest='cleanup',
                       action="store_true", default=False,
                       help='Cleanup chanllenge')

options = optparser.parse_args()     

if options.cleanup:
    if os.path.exists(ACME_CHALLENGE_PATH):
        print('Build HTTP authentication')
        # Create token file for web server
        if not os.path.exists(ACME_CHALLENGE_PATH):
        token_path = os.path.join(ACME_CHALLENGE_PATH, CERTBOT_TOKEN)

        with open(token_path, 'w') as token:

response = requests.get(LIVEDNS_API + "zones", headers=headers)

target_zone = None
if (response.ok):
    zones = response.json()
    for zone in zones:
        if zone['name'] == DOMAIN:
            target_zone = zone

if not target_zone:
    print('Any zone found for domain %s' % (DOMAIN))

domain_records_href = target_zone['zone_records_href']

# Get TXT record
response = requests.get(domain_records_href + "/" + ACME_RECORD, headers=headers)

# Delete record if it exists
if (response.ok):
    requests.delete(domain_records_href + "/" + ACME_RECORD, headers=headers)

if options.cleanup:

print('Build DNS authentication')
record = {
    "rrset_name": ACME_RECORD,
    "rrset_type": "TXT",
    "rrset_ttl": 300,
    "rrset_values": [CERTBOT_VALIDATION],

response =,
                         headers=headers, json=record)

if (response.ok):
    print("DNS token created")
    print("Something went wrong")

A downloadable version is available here

In /etc/crontab :

0  1   1 * *   root   certbot renew --manual-auth-hook /root/ --manual-cleanup-hook /root/

Where /root/ is


/root/ --cleanup

And in /etc/letsencrypt/renewal-hooks/post/ :


service nginx restart
Thursday, 14 February 2019
Écrit par
Grégory Soutadé

Today we'll play a bit with Git. At work, we make some products that uses customized Linux kernel. Once deployed, this kernel is not often updated, so we chose to be based on LTS (Long Term Support) kernels. This gives us staibility and not so many rebase to do. Unfortunately, kernel gets security patches that we must include into our development.

But, to keep clear history, we want to have all our commits in top of the vanilla branch. Plus, having this schema helps to extract all customs patches for Yocto or other build system.

For our case, history needs to be rewrote in a non trivial way.

We currently work with version v4.14.59, but nowaday, has submitted revision v4.14.98. Lets says that we have made the following commits

6f0b0d94b3e2250551fac6ba58b5ec7a02714174 --> 0790c6bd39a86b3964d022746fc85ae2eefb824d

after tag v4.14.59. So, we have something like this :

Current git state

In our remotes we have :

  • upstream --> points to
  • origin --> internal copy of

Our branches are :

  • linux-4.14.y -> upstream/linux-4.14.y (LTS branch)
  • linux-4.14.y-custom -> origin/linux-4.14.y-custom

First, we need to update LTS branch

    git checkout linux-4.14.y
    git pull upstream linux-4.14.y
    git fetch --tags upstream

The trick here is to put the HEAD of our custom branch at the last tag without deleting our commits. So, we need to make a copy of this one.

    git checkout linux-4.14.y-custom
    git checkout -b linux-4.14.59-custom linux-4.14.y-custom

Then, cut the the HEAD and integrate vanilla work.

    git reset --hard v4.14.59
    git rebase linux-4.14.y

Finally, integrate back our commits.

    git cherry-pick 6f0b0d94b3e2250551fac6ba58b5ec7a02714174 .. 0790c6bd39a86b3964d022746fc85ae2eefb824d

The work is almost finished, we still need tu update internal tags we made ! Unlike subversion, a tag in git is just a reference to a specific commit, so it's easy to manage and update. Even if it's a shared repository, we can change them because people that uses them are focused on our custom commits and not on the ones in vanilla branch. Here is a script that get all custom tags references and apply them to the cherry picked commits. An other strategy could be to postfix tags with the new kernel revision. It's up to you to decide what better fit your needs.

The script assume all our custom tags starts with "customXXX".



nb_commits=`git log --pretty=oneline $OLD_START..$OLD_END|wc -l`

for tag in `git tag -l $TAGS_PREFIX`; do
    cur_commits=`git log --pretty=oneline $OLD_START..$tag|wc -l`
    new_commit=`git log --pretty="format:%H" -n1 --skip=$(($nb_commits - $cur_commits)) $NEW_START..HEAD`
    # git log --pretty=oneline -n1 $new_commit
    git tag -d $tag
    git tag $tag $new_commit

Last thing to do, is to sync with remote. We need to pull from origin because HEAD had a strange behavior :

    git pull origin linux-4.14.y-custom
    git push origin linux-4.14.y-custom
    git push origin linux-4.14.59-custom # Optional
    git push --force --tags origin

Force for pushing tags is not needed if the tags were not modified, but just created. Now, we can delete our copy branch or keep it into git. Don't delete it, if you want to keep your old tags references !

Final result :

Final result

Thursday, 17 January 2019
Écrit par
Grégory Soutadé

After further investigations, I found a correct fix to this. In facts, its my configuration that is wrong. I have LUKS on LVM schema like this :

system_group (LVM)
    --> system_crypt (LUKS)
swap_group (LVM)
    --> swap_crypt (LUKS)
home_group (LVM)
    --> home_crypt (LUKS)

To be right handled, you need to declare :

In /etc/crypttab :

home--group-home_crypt UUID=349ca075-2922-4c9c-a52b-8dce587767ea /root/home.key luks
swap--group-swap_crypt UUID=4490ce3c-8700-4e90-81df-250cd3573b7c /root/swap.key luks
system--group-system_crypt UUID=95e39100-25c2-41be-829a-bd84fcb21d0a none luks

use blkid command to get right UUID values

In /etc/fstab :

/dev/mapper/system--group-system_crypt /           ext4    errors=remount-ro 0       1
UUID=6866a661-0424-472c-853e-6daa20d15d74 /boot    ext4    defaults          0       2
/dev/mapper/home--group-home_crypt /home           ext4    defaults          0       2
/dev/mapper/swap--group-swap_crypt none            swap    sw                0       0

In /etc/initramfs-tools/conf.d/resume :


Then, do sudo update_initramfs -u and restart.

It's the second time it happens ! After an update my Debian refuse to boot. This time, I wasn't asked for anything !!

I have an LUKS on LVM configuration. I don't use UUID in my cryptroot and Debian scripts only activate devices with it ! I need to manually add "vgchange" command to mount all devices. Unfortunately, the patched script was overwrote by update.

If it happens, follow this procedure :

  • At boot, wait for the rescue shell (~5 minutes)
  • Enter "vgchange -ay"
  • Enter "exit"

Now, you may be able to boot into your system. Then :

  • Edit /usr/share/initramfs-tools/scripts/local-top/cryptroot
  • Add "vgchange -ay" in wait_for_source() function
  • Update initramfs with "sudo update-initrmafs -u"

Safely reboot !

Monday, 06 August 2018
Écrit par
Grégory Soutadé

Here is a simple tip to use keyctl in a bash script. keyctl is a wrapper for Linux kernel key management interface. It allows to securely save data in kernel memory. The man documentation is very bigcomplete but I didn't find any example on internet. What I initially wanted to do is to safely store a password entered by user inside a bash shell script and keep private to it (don't share with other processes).

Basically the script looks like :



keyctl new_session > /dev/null
keyid=`keyctl add user mail $password @s`
keyctl show
# echo "KEYID $keyid"
keyctl print $keyid

The first thing to do is to create a new session (to detach the current shared one).

Then we will add the password in the new item "mail". We don't have other choice to set type to "user". The item will be placed into the session keyring (@s). We could create new keyrings to store it with keyctl newring command. The command return item id as a big integer. We can use this integer or its name "%user:mail" for further references.

There is also a command keyctl padd which read data from stdin, but I don't recommend to use it as data is displayed in clear on the terminal.

Finally we show keyring information and print our password. We use print command to have an human friendly output, keyctl read command display it in hex format...

Friday, 27 July 2018
Écrit par
Grégory Soutadé

Today, a small Python script to track live stock exchanges. It fetch data from boursorama website and format it for "Generic Monitor" XFCE applet which allows to display result of a command line script. Just setup the path of this script in genmon properties and set the delay to 60s (to avoid flooding website).


# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <>.

import requests
import json

params_gettickseod = {"symbol":"%s","length":"1","period":"0","guid":""}
params_updatecharts = {"symbol":"%s","period":"-1"}

base_headers = {
    'Host': '',
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'fr,en-US;q=0.7,en;q=0.3',
    'DNT': '1',
    'Upgrade-Insecure-Requests': '1',
    'Pragma': 'no-cache',
    'Cache-Control': 'no-cache',
base_address = ''

headers = {
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0',
    'Accept': 'application/json, text/javascript, */*; q=0.01',
    'Accept-Language': 'fr,en-US;q=0.7,en;q=0.3',
    'Accept-Encoding': 'gzip, deflate, br',
    'Referer': '',
    'Content-Type': 'application/json; charset=utf-8',
    'X-Requested-With': 'XMLHttpRequest',
    'DNT': '1',
    'Connection': 'keep-alive',

xhr_address = ''
address_gettickseod = xhr_address + 'GetTicksEOD'
address_updatecharts = xhr_address + 'UpdateCharts'

cookies = None

def _do_request(address, params, headers):
    if cookies is None:
        req = requests.get(address, params=params, headers=headers)
        req = requests.get(address, params=params, headers=headers, cookies=cookies)

    if req.status_code ==
        j = req.json()
        if len(j) == 0:
            raise Exception('Not available')
        return j
        raise Exception("Request error!")

def getStock(stock, display_name=None):
    my_headers = headers.copy()
    my_headers['Referer'] = headers['Referer'] % (stock)

    close_value = 0
    res = ''

    my_params  = params_updatecharts.copy()
    my_params["symbol"] = stock
        j = _do_request(address_updatecharts, my_params, my_headers)
        req = requests.get(base_address + stock, headers=base_headers)
        # cookies = req.cookies
        j = _do_request(address_updatecharts, my_params, my_headers)

    current = float(j['d'][0]['c'])
    my_params  = params_gettickseod.copy()
    my_params["symbol"] = stock
        j = _do_request(address_gettickseod, my_params, my_headers)
        close_value = float(j['d']['qv']['c'])
    except Exception, e:
        if not len(j):
            raise e
        close_value = float(j['d'][0]['o']) # Open value

    if close_value != 0:
        var = ((current/close_value) - 1)*100
        var = 0
    if current < close_value:
        color = 'red'
        var = -var
        color = 'green'
    if not display_name is None:
        res += '%s ' % (display_name)
    res += '%.3f <span fgcolor="%s">%.2f</span>' % (current, color, var)

    return res

def getMail():
    res = ''
    nb_messages = ''
    pipew = open("/tmp/gmail-pipe-w", "wb+")
    piper = open("/tmp/gmail-pipe-r", "rb+")
    while not len(nb_messages):
        nb_messages = piper.readline()
    if len(nb_messages):
        nb_messages = int(nb_messages)
        if nb_messages == 1:
            res = ', 1 msg'
        elif nb_messages > 1:
            res = ', %d msgs' % (nb_messages)

    return res

def getStocks(stocks):
    res = ''
    for stock in stocks:
        if res != '': res += ', '
            res += getStock(*stock)
        except Exception, e:
            if len(stock) > 1:
                res += "%s %s" % (stock[1], str(e))
                res += str(e)
    res += getMail()
    print('<txt>%s</txt>' % (res))

getStocks([('1rPENX', 'Euronext'), ('1rPAIR',)])

Get stock code id from website URL (last part). A file version is available here.

I added another part to get email count from gmail. It relies on a bash script that fetches RSS feeds when data is wrote in the FIFO.

Body of the script :



while [ 1 ] ; do
    echo -n "Please enter gmail account password : "
    read -s password
    echo ""
    echo -n "Confirm password : "
    read -s password2
    echo ""
    if [ "$password" != "$password2" ] ; then
        echo -e "Passwords doesn't match !!\n"


rm -f $pipew $piper
mkfifo $pipew $piper

while [ 1 ] ; do
    read line < $pipew
    feeds=`curl -u "$USER:$password" --silent ""`
    echo $feeds | sed  s/.*\<fullcount\>//g | sed  s/\<\\/fullcount\>.*//g > $piper

You can hardcode password in the script, but I don't like having my password in clear on the harddrive. A file version is available here.