Monter une PKI complète avec cfssl

Le dans «Non catégorisé» par Eldeberen

Monter une PKI1 basée sur X.509 est souvent assez compliqué. Dès qu'on sort des sentiers battus, on se retrouve vite à passer pas mal de temps pour essayer d'avoir un truc pas trop bancal. J'ai passé pas mal de temps à débugguer ça au taf, donc je pose ça ici pour pas perdre de trace.


Architecture cible

L'architecture de la PKI qu'on va monter sera celle-ci :

Architecture cible

On a donc une Root CA2 qui signe une CA intermédiaire, qui elle même va signer des certificats qui seront utilisés pour authentifier des services. Au niveau des TTL3, j'ai arbitrairement mis 5 ans pour la Root CA, 3 ans pour l'intermédiaire et 3 mois pour les certificats.

Les outils utilisés

Pour éviter de (trop) se prendre la tête avec openssl, on va utiliser l'outil cfssl, développé par CloudFlare. On gardera quand même openssl pour vérifier que tout va bien à l'arrivée.

Schéma des relations entre fichiers

Les fichiers de configuration sont en jaune, les fichiers temporaires de CSR4 en rouge, et les fichiers cible en blanc.

Génération

Root CA

Le fichier de config utilisé est le suivant :

# root.json

{
    "CN": "MiddleEarth Root CA",
    "key": {
        "algo": "rsa",
        "size": 4096
    },
    "names": [{
        "C": "FR",
        "L": "Lyon",
        "O": "MiddleEarth"
    }],
    "ca": {
        "expiry": "43830h"
    }
}
  • CN : Common Name, le nom qui sera attribué à la CA
  • key : les paramètres de la clé, ici RSA par compatibilité avec des systèmes embarqués qui supportent pas l'ECC5
  • names : un nom d'organisation, optionnel
  • ca : des paramètres spécifiques à une CA

On peut passer à la génération de la CA :

$ cfssl gencert -initca root.json | cfssljson -bare root
  • gencert indique que l'on génère un certificat (et non une demande de certificat)
  • -initca qu'on souhaite initialiser la CA, c'est à dire l'auto-signer
  • root.json notre fichier de config

Le pipe derrière sert à récupérer le Json renvoyé par cfssl pour en extraire les fichiers au format PEM7.

Trois fichiers ont été créés, respectivement root.pem, root-key.pem et root.csr. Le premier est le certificat, le second la clé privée, le troisième la requête de signature. On peut ensuite les renommer en root.crt et root.key, ou ce que vous voulez en fait.

Un coup de openssl permet de s'assurer qu'on a bien un certificat autosigné :

$ openssl x509 -in root.crt -noout -subject -issuer
subject=C = FR, L = Lyon, O = MiddleEarth, CN = MiddleEarth Root CA
issuer=C = FR, L = Lyon, O = MiddleEarth, CN = MiddleEarth Root CA

openssl x509 -in root.crt -noout -text pour avoir un aperçu plus complet.

Intermediate CA

Il va y avoir deux étapes : la génération d'une CSR puis la signature du certificat par la Root CA.

CSR

On commence par définir les paramètres de notre CA intermédiaire :

# inter.json

{
    "CN": "MiddleEarth Intermediate CA",
    "key": {
        "algo": "rsa",
        "size": 4096
    },
    "ca": {
        "expiry": "26298h"
    }
}

Rien de plus que pour la Root CA, donc je ne m'étale pas dessus.

Avec ça on peut générer une CSR :

$ cfssl genkey -initca inter.json | cfssljson -bare inter
  • genkey pour indiquer qu'on souhaite générer une CSR et non pas directement le certificat

Trois fichiers ont été créés : inter.pem, inter-key.pem et inter.csr. Ce dernier fichier servira à signer le certificat par la Root CA.

Certificat

Il faut définir des paramètres spécifiques à la signature :

# inter-sign.json

{
    "signing": {
        "default": {
            "expiry": "26298h",
            "usages": [
                "signing",
                "cert sign",
                "crl sign"
            ],
            "ca_constraint": {
                "is_ca": true
            }
        }
    }
}
  • signing : les paramètres de signature
  • default : le profil par défaut, j'y reviendrais
  • expiry : le TTL d'expiration de la signature
  • usages : les permissions déléguées à la CA
  • ca_constraint : on indique que c'est bien une CA

On génère nos cibles :

$ cfssl sign -ca root.crt -ca-key root.key --config inter-sign.json inter.csr | cfssljson -bare inter
  • sign car on veut signer un certificat
  • -ca pour spécifier le certif de la CA qui signe
  • -ca-key pour la clé de la CA qui signe
  • -config pour les paramètres de signature
  • inter.csr la CSR précédemment générée

Cette fois-ci on se retrouve bien avec inter.pem, inter-key.pem. Je renomme les fichiers en inter.crt et inter.key par cohérence. J'en profite pour faire un cat inter.crt root.crt > inter-fullchain.crt qui sera utilise pour avoir la chaine de certification par la suite.

Là aussi, quelques commandes permettent de s'assurer de la validité de la chaine :

$ openssl x509 -in inter.crt -noout -subject -issuer
subject=CN = MiddleEarth Intermediate CA
issuer=C = FR, L = Lyon, O = MiddleEarth, CN = MiddleEarth Root CA

$ openssl verify -CAfile root.crt inter.crt
inter.crt: OK

En cas de besoin, vous pouvez répéter le processus pour faire autant niveau de sous CA que nécessaire.

Certificat final

La config du certificat :

# certif.json

{
    "CN": "middleearth.fr",
    "hosts": [
        "blog.middleearth.fr",
        "*.wildcard.middleearth.fr"
    ],
    "key": {
        "algo": "rsa",
        "size": 4096
    }
}
  • hosts est une liste de SAN6

Et la config des paramètres de signature :

{
    "signing": {
        "default": {
            "expiry": "720h",
            "usages": ["key encipherment", "server auth"]
        }
    }
}

Vu que c'est un simple certificat et non une CA, on peut direct générer le tout :

$ cfssl gencert -ca=inter.crt -ca-key=inter.key -config=certif-sign.json certif.json | cfssljson -bare certif

Ce qui nous donne trois fichiers, certif.pem, certif-key.pem et certif.csr. On va garder les deux premiers sous les noms certif.crt et certif.key. Et là aussi j'en profite pour le cat inter-fullchain.crt certif.crt > certif-fullchain.crt.

Pour vérifier que tout va bien :

$ openssl x509 -in certif.crt -noout -subject -issuer
subject=CN = middleearth.fr
issuer=CN = MiddleEarth Intermediate CA

$ openssl verify -CAfile root.crt -untrusted inter.crt certif.crt
certif.crt: OK

Conclusion

Par sécurité on mettra les fichiers de la Root CA en sécurité hors-ligne sauvegardé dans plusieurs endroits de confiance.

La CA intermédiaire peut être laissée sur un serveur sécurisé qui servira à renouveler les certificats du service.

Et le certificat fullchain final peut être placé dans la config d'un serveur web par exemple. :)


Comme promis, le gros Makefile que j'ai fait au taf.

# End purpose targets
.PHONY: root
root: root/CA.pem
    @echo "\033[0;32mRoot CA generation completed\033[0m"

.PHONY: dev
dev: dev/subCA.pem dev/vault.pem
    @echo "\033[0;32mDev intermediate CA generation completed\033[0m"
.PHONY: stg
stg: stg/subCA.pem stg/vault.pem
    @echo "\033[0;32mStg intermediate CA generation completed\033[0m"
.PHONY: prd
prd: prd/subCA.pem prd/vault.pem
    @echo "\033[0;32mPrd intermediate CA generation completed\033[0m"

# Verify certificate chain: `make verify ENV=dev`
.PHONY: verify
verify:
    openssl x509 -in root/CA.pem -noout -issuer -subject
    openssl x509 -in ${ENV}/subCA.pem -noout -issuer -subject
    openssl x509 -in ${ENV}/vault.pem -noout -issuer -subject -text
    openssl verify -CAfile root/CA.pem ${ENV}/subCA.pem
    openssl verify -CAfile root/CA.pem -untrusted ${ENV}/subCA.pem ${ENV}/vault.pem


# Export files to a specified volume
.PHONY: export
export: root.tar.xz.gpg
    mkdir -p ${DEST}
    cp $< ${DEST}
    cp -r $(foreach env,dev stg prd,${env}) ${DEST}


# Clean all certificates and private keys
.PHONY: clean
clean:
    rm $(foreach env,root dev stg prd,$(foreach ext,csr crt pem,$(wildcard ${env}/*.${ext})))


# Root CA generation
root/CA.pem: root/config.json
    @echo "\033[0;33mGenerate root CA private key\033[0m"
    cfssl gencert -initca $< | cfssljson -bare $(basename $@)

root.tar.xz: root/CA.pem
    @echo "\033[0;33mGenerate root archive bundle\033[0m"
    tar -cvJf $@ root/


# Intermediate CA generation
%/subCA.csr: %/intermediate.json | root
    @echo "\033[0;33mGenerate intermediate CA private key ($(@D))\033[0m"
    cfssl gencert -initca $< | cfssljson -bare $(basename $@)

%/subCA.pem: %/subCA.csr %/sign-intermediate.json | root
    @echo "\033[0;33mSign intermediate CA private key ($(@D))\033[0m"
    cfssl sign -ca root/CA.pem -ca-key root/CA-key.pem --config $*/sign-intermediate.json $< | cfssljson -bare $(basename $@)

%/subCA-fullchain.pem: %/subCA.pem
    @echo "\033[0;33mConcatenate fullchain subCA certificate ($(@D))\033[0m"
    cat root/CA.pem $*/subCA.pem > $@


# Certificates for Vault
%/vault.pem: %/subCA.pem %/vault.json | %
    @echo "\033[0;33mGenerate certificate for Vault ($(@D))\033[0m"
    cfssl gencert -ca=$*/subCA.pem -ca-key=$*/subCA-key.pem -config=$*/sign-vault.json $*/vault.json | cfssljson -bare $(basename $@)


# Utils
%.crt: %.pem
    @echo "\033[0;33mExport public certificate ($(@D))\033[0m"
    openssl x509 -in $< -out $@

%.gpg: %
    @echo "\033[0;33mEncrypt archive\033[0m"
    gpg -c $<

Les fichiers de conf sont à peu près les mêmes, j'ai juste séparé les sub-CA par environnement (development, staging et production). Les certificats sont générés pour Vault uniquement, puisqu'ensuite on utilise cet outil pour gérer les certificats sur le reste de l'infra à coup d'API.

.
├── dev
│   ├── intermediate.json
│   ├── sign-intermediate.json
│   ├── sign-vault.json
│   └── vault.json
├── Makefile
├── prd
│   ├── intermediate.json
│   ├── sign-intermediate.json
│   ├── sign-vault.json
│   └── vault.json
├── root
│   └── config.json
└── stg
    ├── intermediate.json
    ├── sign-intermediate.json
    ├── sign-vault.json
    └── vault.json

  1. Public Key Infrastructure, l'infrastructure de gestion des clés/certificats 

  2. Certificate Authority, une autorité de certification 

  3. Time To Live, la durée de validité 

  4. Certificate Signing Request, une demande de signature 

  5. Elliptic Curve Cryptography, la cryptographie elliptique 

  6. Subject Alternative Name, des noms alternatifs comme des domaines supplémentaires, etc. 

  7. Privacy-Enhanced Mail, un format de fichier prévu pour l'échange de clés et de certificats cryptographiques 

Vous pouvez réagir à cet article sur le Fédivers, en citant @Darks@social.darks.fr :)