Quelques outils pour gérer les clés secrètes en Django


On ne veut pas mettre sa SECRET_KEY en prod, et utiliser un service pour générer la clé, ça va deux minutes.

Générer une clé secrète:

import random
import string
 
def secret_key(size=50):
    pool = string.ascii_letters + string.digits + string.punctuation
    return "".join(random.SystemRandom().choice(pool) for i in range(size))

Générer une clé secrete avec une commande manage.py:

from django.core.management.base import BaseCommand, CommandError
from polls.models import Question as Poll
 
class Command(BaseCommand):
    help = 'Generate a secret key'
 
    def add_arguments(self, parser):
        parser.add_argument('size', default=50, type=int)
 
    def handle(self, *args, **options):
        self.stdout.write(secret_key(options['size']))

A mettre dans ./votreapp/management/command/generate_secret_key.py :)

Une fonction pour lire la clé depuis un fichier texte ou générer la clé si elle n’existe pas:

import io
import os
 
try:
    import pwd
except ImportError:
    pass
 
try:
    import grp
except ImportError:
    pass
 
 
def secret_key_from_file(
        file_path, 
        create=True, 
        size=50, 
        file_perms=None, # unix uniquement
        file_user=None, # unix uniquement
        file_group=None # unix uniquement
    ):
    try:
        with io.open(file_path) as f:
            return f.read().strip()
    except IOError as e:
        if e.errno == 2 and create:
            with io.open(file_path, 'w') as f:
                key = secret_key(size)
                f.write(key)
 
            if any((file_perms, file_user, file_group)) and not pwd:
                raise ValueError('File chmod and chown are for Unix only')
 
            if file_user:
                os.chown(file_path, uid=pwd.getpwnam(file_user).pw_uid)
 
            if file_group:
                os.chown(file_path, gid=grp.getgrnam(file_group).gr_gid)
 
            if file_perms:
                os.chmod(file_path, int(str(file_perms), 8))
 
            return key
 
        raise

Et une fonction pour récupérer la clé depuis une variable d’environnement ou un fichier:

def get_secret_key(
        file_path=None, 
        create=True, 
        size=50, 
        file_perms=None, 
        file_user=None, 
        file_group=None,
        env_var="DJANGO_SECRET_KEY"
    ):
    try:
        return os.environ[env_var]
    except KeyError:
        if file_path:
            return secret_key_from_file(file_path, create=create, size=size)
        raise

Le but de cette dernière est d’avoir ça dans son fichier de settings:

SECRET_KEY = get_secret_key('secret_key')

Et de foutre ‘secret_key’ dans son .gitignore.

Comme ça:

  • Si on n’a pas de clé secrète, on en génère une.
  • Si on a une, elle est dans un fichier qui n’est PAS dans settings.py.
  • On peut commiter settings.py. Chaque env de dev et prod a sa clé secrète automatiquement.
  • On peut overrider la clé avec une variable d’environnement si on le souhaite.

En attendant, j’ai proposé qu’on ajoute ça a django extensions. Et qui sait, dans le core peut être un jour ?

10 thoughts on “Quelques outils pour gérer les clés secrètes en Django

  • Oscar LASM

    Salut,

    Moi j’utilise django-environ pour gérer mes settings

  • Sam Post author

    @luxcem : secret.choice est un alias de ce que j’utilise dans secret_key(). Mais c’est vrai que c’est une bonne pratique de l’utiliser si on sait qu’il est dispo.

    @Oscar LASM: c’est bien si tu utilise les env vars. Mais je m’appercois de plus en plus que 90% des devs ne savent même pas ce que c’est. Déjà sous windows…

  • paulo

    Salut Sam,

    Désolé suis un débutant, mais une question me taraude.

    quelle différence cela fait t’il entre un clef visible ( SECRET_KEY =’bvqsdhkjh…”), et une clef à priori cachée (SECRET_KEY=get_secret_key(‘secret_key’))

    ne peut t’on pas de toute façon affiché SCRET_KEY?

    et pourquoi du coup ne pas faire en python 3.6 :

    from secrets import token_urlsafe

    SECRET_KEY = token_urlsafe(37)

    la clef serait différente à chaque démmarage

    merci d’avance

  • Sam Post author

    Ce n’est pas une question de visibilité, c’est juste pour éviter de gérer la génération de la clé sur chaque poste. Si tu as 3 dev, 3 serveurs de prod, un de dev et un de staging, ça fait 8 clés. Si tu utilise ça la clé est générée automatiquement et tu n’as pas à t’en soucier.

    Ca évite aussi l’erreur classique de la clé commitée dans git par le stagiaire si secret_key est déjà dans le .gitignore ou la mise en prod où on oublie la clé.

    Enfin on ne doit surtout pas changer la clé à chanque lancement, car les mots de passes en bdd de django.contrib.auth sont hashés avec la clé, et un changement invaliderait tous les mots de passe.

  • paulo

    Merci à toi Max

    parfois les débutants, veulent absolument trouver des trucs simple avant les vrais dev lol

  • Matthieu

    J’ai tenté de changer la clé, j’ai cassé ma session en cours, mais j’ai pu me reconnecter avec le même mot de passe.

    Peut être que ce sont des settings additionnels qui lient les MdP à la clé.

  • paulo

    Une autre petite question,

    si une machine redémarre, les variables d’environnements sont t’elles sauvegardées?

  • little-indian

    @paulo: Tes variables d’environnements sont sauvegardées du moment que tu les mets dans un fichier et que tu sources celui-ci au démarage de ton contexte d’exécution …. pour cela plusieurs possibilités :

    – les mettre dans le bashrc de ton utilisateur

    – les mettre dans un fichier X ou Y et sourcer celui-ci à la volée et ensuite lancer ton appli

    – etc…

    c’est le même mécanisme que quand tu utilises des virtualenv par exemple…

  • Matthieu

    M’étant rendu compte que j’avais pas mal de choses en commun dans mes différents projets django, je me suis fait une surcouche, qui permet entre autres de s’occuper facilement des settings.

    Ma surcouche définit pas mal de settings par défaut (a priori viables, genre USE_TZ = True), ensuite mon projet donne d’autres settings (ou écrase certains précédents) et je cherche enfin dans des fichiers de conf (./local_settings.{py|ini} et $VIRTUALENV/etc/monprojet/settings.{py|ini}). On a ensuite une fusion de ces 6 différentes sources de settings.

    Au final :

    la grande majorité des settings sont définis une seule fois dans mon socle applicatif,
    quelques settings sont overridés dans chaque projet (parfois seulement 3 ou 4),
    en dév’, j’ai uniquement un fichier supplémentaire (local_settings.py) à la racine du projet contenant en général DEBUG=True,
    quand je déploie (avec pip install monprojet, rien de plus), je dois écrire un fichier de conf’ normal (au format .ini), sachant que j’ai une commande Django qui me donne son emplacement et son contenu avec les valeurs actuelles,
    je n’ai plus à gérer plusieurs fichiers de settings avec 99% de choses en commun et qu’il faut mettre à jour à chaque modif,
    les mises à jour se font via pip install monprojet --upgrade (avec migrate et collecstatic) et n’affectent pas le fichier de conf local.

    Après la fusion, il y a une analyse des settings, pour faire des références entre settings, créer automatiquement des dossiers, … sachant que toutes les strings sont par défaut interprétées comme des string.format.

    Par exemple, les settings fournis par mon socle définissent :

    DATABASES = {'default': {
        'ENGINE': CallableSetting(database_engine, 'DATABASE_ENGINE'), 'NAME': '{DATABASE_NAME}', 'USER': '{DATABASE_USER}',
        'PASSWORD': '{DATABASE_PASSWORD}', 'HOST': '{DATABASE_HOST}', 'PORT': '{DATABASE_PORT}'},
    }

    Quand je déploie, j’ai uniquement à écraser DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, DATABASE_HOST et DATABASE_PORT dans le fichier de settings local (les valeurs par défaut utilisent du sqlite, histoire d’avoir un truc utilisable par défaut).

    J’ai également une liste de correspondances entre les settings Python à personnaliser (genre DATABASE_NAME) et le format .ini (database.name, pour chercher l’option name dans la section [database]).

    Grâce à la possibilité d’appeler du code après la fusion, je peux faire des choses assez complexes : si je définis les coordonnées Redis dans la section [cache] ou celles du LDAP dans la section [auth], j’ajoute les packages à INSTALLED_APPS et je vérifie que les packages sont installés.

    Et pour revenir au sujet, SECRET_KEY est défini par un callable qui tente de lire un fichier, et s’il n’existe pas, le crée en générant son contenu => une nouvelle clef est générée lors de chaque déploiement et reste persistante sur la machine, de façon transparente.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.