Comment mettre en place une PKI avec Vault et délivrer des certificats TLS via de simples appels API ?

Temps de lecture : 9 minutes

Introduction : Public Key Infrastructure

Une PKI (Public Key Infrastructure) ou IGC (Infrastructure de Gestion de Clés en français), est un système permettant de délivrer et de gérer de certificats numériques. Les certificats ont de nombreuses applications : ils peuvent être utilisés pour authentifier des machines ou des personnes, signer ou chiffrer des données, etc. 

On ne peut pas ignorer les avantages/gains en sécurité qu’offrent les IGC, cependant les entreprises hésitent souvent à se lancer dans ces projets réputés pour leur complexité. Ce n’est pas tant l’expertise technique qui les effraie mais plutôt les contraintes au niveau organisationnel.

Les utilisateurs (administrateurs système, gestionnaires d’applications, etc.) peuvent aussi trouver lourds les processus de demande ou de renouvellement de certificats. Il s’agit souvent de processus manuels passant par la génération d’une clé privée et d’une CSR (Certificate Signing Request ou Demande de signature de certificat en français), la soumission de la demande à une autorité de certification pour signature, l’attente des vérifications et de la délivrance du certificat. En effet, ces processus jugés lourds et complexes font qu’on a souvent beaucoup de certificats auto-signés qui sont générés et utilisés par de nombreux services/applications au sein des entreprises.

Dans les lignes qui suivent nous verrons comment mettre en place rapidement une IGC en utilisant la solution Vault de HashiCorp et délivrer facilement des certificats TLS via des simples appels API.

Architecture

L’architecture (cf. schéma ci-dessous) que nous mettrons en place sera toute simple. Elle comprendra une autorité de certification racine et une autorité intermédiaire qui sera utilisée pour délivrer des certificats finaux. 

Mise en place de la PKI

Dans le cadre de cette mise en place de PKI nous ne rentrerons pas dans l’installation de Vault. Nous supposons que l’infrastructure Vault est déjà installée, configurée et opérationnelle. 

Lorsqu’on vérifie le statut opérationnel de Vault, on doit constater qu’il a été initialisé et descellé avant de passer aux étapes de configurations de l’IGC :

# vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    5
Threshold       3
Version         1.2.2
Cluster Name    vault-cluster-ee142db2
Cluster ID      68899c49-f90d-4ae2-d500-94b078e66d77
HA Enabled      false

Par défaut tous les “secrets engines” ne sont pas montés (activés). Commençons par vérifier ceux qui sont présents :

# vault secrets list
Path          Type         Accessor              Description
----          ----         --------              -----------
cubbyhole/    cubbyhole    cubbyhole_c6de4f85    per-token private secret storage
identity/     identity     identity_1b36e529     identity store
sys/          system       system_ef00c4fb       system endpoints used for control, policy and debugging

Comme vous pouvez le remarquer,  il n’y a pas de “secrets engines” PKI activés. Donc nous devons les activer. 

Activation des backends pki

Il est important de retenir qu’il faut activer le “secret engine” PKI autant de fois qu’il y aura d’autorités de certification. Dans notre cas, elle sera activée 2 fois.

# vault secrets enable -path=rootca -description=”PKI backend for Root CA” -max-lease-ttl=87600h pki
Success! Enabled the pki secrets engine at: rootca/
# vault secrets enable -path=interca -description="PKI backend for Intermediate CA" -max-lease-ttl=87600h pki
Success! Enabled the pki secrets engine at: interca/

Si nous listons les “secrets engines” activés, nous devons en noter 2 nouveaux de type pki.

# vault secrets list
Path          Type         Accessor              Description
----          ----         --------              -----------
cubbyhole/    cubbyhole    cubbyhole_c6de4f85    per-token private secret storage
identity/     identity     identity_1b36e529     identity store
interca/      pki          pki_7c8ec09c          n/a
rootca/       pki          pki_875994d8          n/a
sys/          system       system_ef00c4fb       system endpoints used for control, policy and debugging

Création de l’AC racine

# vault write rootca/root/generate/internal common_name=Test-Root-CA ttl=87600h key_bits=4096

Vérifions que tout s’est bien passé en regardant précisément le CN, les dates de validité et la longueur de clés :

# curl -s http://<VAULT_IP>:8200/v1/rootca/ca/pem | openssl x509 -text

Extrait du résultat :

     Serial Number:
            07:46:ea:66:49:1f:08:58:37:1c:9c:18:25:1b:cf:c9:7c:78:24:fb
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = Test-Root-CA
        Validity
            Not Before: Aug 26 16:36:34 2019 GMT
            Not After : Aug 23 16:37:02 2029 GMT
        Subject: CN = Test-Root-CA
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (4096 bit)

Configuration de l’AIA et du CDP :

# vault write rootca/config/urls issuing_certificates="https://<VAULT_IP>:8200/v1/rootca/ca" crl_distribution_points="https://<VAULT_IP>:8200/v1/rootca/crl"

Création de l’AC intermédiaire

Génération de la CSR de l’AC intermédiaire : 

# vault write -format=json interca/intermediate/generate/internal common_name=Intermediate-Technical-CA ttl=43800h key_bits=4096 | tee \
>(jq -r .data.csr > interca.csr) \
>(jq -r .data.private_key > interca.pem)

Signature de la CSR de l’AC intermédiaire par l’AC racine et génération du certificat :

# vault write -format=json rootca/root/sign-intermediate \
csr=@interca.csr \
common_name=Intermediate-Technical-CA ttl=43800h key_bits=4096 | tee \
>(jq -r .data.certificate > interca.pem) \
>(jq -r .data.issuing_ca > interca_issuing_ca.pem)

Importation du certificat généré au niveau de l’AC intermédiaire :

# vault write interca/intermediate/set-signed certificate=@interca.pem

Vérifions que tout s’est bien passé en regardant précisément le CN, les dates de validité et la longueur de clés :

# curl -s https://<VAULT_IP>:8200/v1/interca/ca/pem | openssl x509 -text

Configuration de l’AIA et du CDP :

 # vault write interca/config/urls \
     issuing_certificates="http://<VAULT_IP:8200>/v1/interca/ca" \
     crl_distribution_points="http://<VAULT_IP:8200/v1/interca/crl

Génération d’un certificat pour le serveur web nginx

Création d’un rôle pouvant générer des certificats finaux :

# vault write interca/roles/issuer-cert-server \
key_bits=2048 \
max_ttl=8760h \
allow_any_name=true

Génération d’un certificat serveur :

# vault write interca/issue/issuer-cert-server \
common_name=my-nginx.test.com \
ttl=2m \
format=pem

Récupérons les valeurs des certificats du serveur et de l’AC émettrice et sauvegardons les dans un fichier unique portant le nom de certificate.pem.

Le contenu de la clé privée sera sauvegardée dans un fichier portant le nom pk.pem

Configuration du serveur web nginx et recette

Les 2 fichiers générés ci-dessus doivent être copiés sur le serveur nginx dans le dossier /etc/nginx/certs.

Les fichiers étant disponibles, il faut maintenant modifier la configuration nginx pour activer TLS.

# Settings for a TLS enabled server.
    server {
        listen       443 ssl http2 default_server;
        listen       [::]:443 ssl http2 default_server;
        server_name  my-nginx.test.com;
        root         /usr/share/nginx/html;

        ssl_certificate "/etc/nginx/certs/certificate.pem";
        ssl_certificate_key "/etc/nginx/certs/pk.pem";
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
        ssl_ciphers PROFILE=SYSTEM;
        ssl_prefer_server_ciphers on;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }

Avant de tester il faudra préalablement faire confiance à l’autorité racine de la PKI que nous venons de mettre en place au niveau du navigateur web client qui sera utilisé pour se connecter au serveur web.

Il faudra également modifier le fichier hosts du poste client utilisé pour pouvoir résoudre le DNS utilisé pour le serveur web.

<IP serveur Nginx>	my-nginx.test.com

Maintenant on peut se rendre en tout sécurité sur le site web en question. Comme nous avons reconnu l’autorité racine de la chaîne de certification, le navigateur ne déclenche pas d’alarme, si nous cliquons sur le petit cadenas, il nous confirme que la connexion est sécurisée et que le certificat est valide.

Si nous allons plus loin en cliquant sur le certificat nous aurons beaucoup d’informations (dates de validité, toute la chaîne de certification, etc.)

Comment automatiser le renouvellement du certificat côté serveur web (client vault) ?

Comme nous venons de le voir, pour demander un certificat web il suffit d’effectuer un simple appel API. Pour l’instant nous l’avons effectué manuellement. Pour nous épargner les processus manuels traditionnels de demande/renouvellement de certificat nous devons automatiser le renouvellement de certificat pour éviter des incidents de production dus à des certificats TLS expirés.

Pour le renouvellement du certificat TLS on sera dans le self-service. Le client (le serveur web nginx) se chargera de renouveler tout seul son certificat. Il doit s’authentifier vis à vis de Vault pour pouvoir soumettre sa demande de certificat.

Pendant que nous sommes sur la PKI, autant profiter de ses différents avantages. On utilisera un module d’authentification de type TLS Certificates pour authentifier les clients initialement. Un autre type de module d’authentification (AppRole par exemple) pourrait être utilisé. 

Coté Vault (en tant qu’admin vault)

Générons le certificat TLS d’authentification initiale  :

# vault write interca/issue/issuer-cert-server \
common_name=my-nginx.auth.test.com \
ttl=8760h \
format=pem

Comme vous pouvez le remarquer, le certificat d’authentification initiale a une durée beaucoup plus longue que le second certificat qui sera utilisé pour authentifier le serveur web lui-même. C’est logique, non ? Si le certificat d’authentification initiale est expiré ou expire au même moment que le second certificat TLS, le serveur ne sera pas en mesure de renouveler le certificat TLS final.

Récupérons la valeur du certificat nouvellement émis et sauvegardons la dans un fichier unique portant le nom de auth.pem.

Le contenu de la clé privée sera sauvegardée dans un fichier portant le nom auth.key

Les 2 fichiers générés ci-dessus doivent être copiés sur le serveur serveur nginx dans le dossier /etc/nginx/certs.

Créons une ACL permettant aux clients d’auto-générer leurs certificats, renouveler des jetons, etc. :

# vi interca.hcl
path "interca/issue/*" {
      capabilities = ["create", "update"]
    }

    path "interca/certs" {
      capabilities = ["list"]
    }

    path "interca/revoke" {
      capabilities = ["create", "update"]
    }

    path "interca/tidy" {
      capabilities = ["create", "update"]
    }

    path "rootca/cert/ca" {
      capabilities = ["read"]
    }

    path "auth/token/renew" {
      capabilities = ["update"]
    }

    path "auth/token/renew-self" {
      capabilities = ["update"]
    }
# vault policy write interca interca.hcl

Créons le module d’authentification de type TLS Certificates :

# vault write auth/cert/certs/web \
    display_name=web \
    certificate=@interca.pem \
    policies="interca" \
    ttl=86400

interca.pem référence le certificat de l’AC intermédiaire (émetrice des certificats TLS finaux).

Les jetons émis par ce module d’authentification sont d’une durée de 86400 secondes (soit 24 h).

Coté serveur web/client vault (en tant qu’admin système)

Pour que le serveur web (client vault) puisse renouveler son certificat, il doit initialement s’authentifier. Il peut s’authentifier via API ou CLI :

Authentification via CLI :

 # vault login -method=cert -ca-cert=interca.pem-client-cert=auth.pem -client-key=auth.key name=web

Authentification via API :

# curl \
    --request POST \
    --cacert interca.pem \
    --cert auth.pem \
    --key auth.key \
    --data '{"name": "web"}' \
    https://[VAULT_IP]:8200/v1/auth/cert/login

Pour automatiser le renouvellement des certificats TLS côté serveurs web, on utilisera un autre outil de HashiCorp qui s’appelle consul-template.

Il est simple à installer :

# wget https://releases.hashicorp.com/consul-template/0.19.5/consul-template_0.19.5_linux_amd64.zip
# yum install unzip
# unzip consul-template_0.19.5_linux_amd64.zip
# mv consul-template /usr/local/bin

Configuration de consul.template coté serveur web :

$ mkdir /etc/consul-template.d/; cd /etc/consul-template.d/
$ vi renew-cert.hcl
vault {
  address = "https://<VAULT_IP>:8200"
  renew_token = true

  retry {
    enabled = true
    attempts = 5
    backoff = "250ms"
  }
}

template {
  source      = "/etc/consul-template.d/certificate.tpl"
  destination = "/etc/nginx/certs/certificate.pem"
  perms       = "0600"
  command     = "systemctl reload nginx"
}

template {
  source      = "/etc/consul-template.d/pk.tpl"
  destination = "/etc/nginx/certs/pk.pem"
  perms       = "0600"
  command     = "systemctl reload nginx"
}

Création des templates déclarés ci-dessus :

# vi /etc/consul-template.d/certificate.tpl

{{- /* certificate.tpl */ -}}
{{ with secret "interca/issue/issuer-cert-server" "common_name=my-nginx.test.com" "ttl=2m" }}
{{ .Data.certificate }}
{{ .Data.issuing_ca }}{{ end }}
# vi /etc/consul-template.d/pk.tpl
{{- /* pk.tpl */ -}}
{{ with secret "interca/issue/issuer-cert-server" "common_name=my-nginx.test.com" "ttl=2m"}}
{{ .Data.private_key }}{{ end }}

Config service systemd :

# vi /etc/systemd/system/consul-template.service
[Unit]
Description=consul-template
Requires=network-online.target
After=network-online.target

[Service]
EnvironmentFile=/etc/sysconfig/consul-template
Restart=on-failure
ExecStart=/usr/local/bin/consul-template -config='/etc/consul-template.d/renew-cert.hcl'
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target
# systemctl daemon-reload
# systemctl enable consul-template.service

Le certificat d’une durée de 2 mn qui a été émis manuellement a entre temps expiré. Si on rafraîchit notre navigateur, nous avons bien un message d’avertissement.

Démarrons le service consul-template :

# systemctl start consul-template.service

Une fois le service démarré avec succès, si nous retournons sur le site web https://my-nginx.test.com/ nous pouvons constater que le message d’avertissement a disparu car le certificat a été renouvelé de façon automatique.

Commentaires :

A lire également sur le sujet :