TLS avec Gateway API: Une gestion efficace et sécurisée des certificats publiques et privés

Sommaire

Le chiffrement TLS est un standard incontournable dans la sécurisation des services et applications, que ce soit sur Internet ou au sein même de l'entreprise. Sur Internet, le recours à un certificat TLS, validé par une autorité de certification reconnue, est essentiel pour garantir la confidentialité des échanges de données.

En ce qui concerne les communications internes, la PKI privée (Private Public Key Infrastructure) joue un rôle crucial dans la distribution et la validation des certificats nécessaires au chiffrement des communications au sein de l'entreprise, assurant ainsi une sécurité renforcée.

Dans cet article, nous allons plonger dans le vif du sujet : la mise en place d'une gestion efficace et robuste des certificats TLS au sein d'une entreprise. Nous explorerons les meilleures pratiques, les outils et les stratégies pour une infrastructure de certificats fiable.

🎯 Notre objectif

  • Afin que les utilisateurs puissent accéder aux applications, nous utiliserons le standard Gateway API. (Je vous invite à lire mon précédent article sur le sujet.)
  • Dans l'implémentation présentée ci-dessus, un composant joue un rôle majeur: Cert-manager. Il s'agit en effet du moteur central qui se chargera de générer et de renouveler les certificats.
  • Pour les applications destinées à rester internes et non exposées sur Internet, nous opterons pour la génération de certificats via une PKI privée avec Vault d'Hashicorp.
  • Quant aux applications publiques, elles utiliseront des certificats délivrés par Let's Encrypt.

🛂 A propos de Let's Encrypt

Basé sur le protocole ACME (Automatic Certificate Management Environment), cette solution permet une installation et un renouvellement automatiques des certificats.
Let's Encrypt est simple à mettre en oeuvre, gratuit et améliore la sécurité. Cependant, il est important de noter que la durée des certificats est courte, et nécessite donc des renouvellements fréquents.
Pour en savoir plus sur son fonctionnement, vous pouvez vous référer à cette documentation.

🔐 Une PKI privée avec Vault

Une PKI privée, ou Infrastructure à Clé Publique privée, est un système cryptographique utilisé au sein d'une organisation pour sécuriser les données et les communications. Elle repose sur une Autorité de Certification (CA) interne qui émet des certificats TLS spécifiques à l'organisation.

Ce système permet à une organisation de :

  • Contrôler entièrement les procédures de vérification de l'identité et de l'authentification, et d'émettre des certificats pour des domaines internes, ce qui n'est pas possible avec Let's Encrypt.
  • Sécuriser les communications et les données internes avec une authentification et un chiffrement forts au sein de l'organisation.

Cependant, la mise en œuvre de ce type d'infrastructure requiert une attention particulière et une gestion de plusieurs composants. Ici, nous allons explorer une des fonctionnalités principales de Vault, qui est initialement un outil de gestion de secrets mais qui peut aussi faire office de PKI interne.

Une plateforme Cloud Native de référence

Toutes les actions réalisées dans cet article proviennent de ce dépôt git

On y trouve le code Opentofu permettant de déployer et configurer Vault mais aussi de nombreuses sources qui me permettent de construire mes articles de blog. N'hésitez pas à me faire des retours, ouvrir des issues si nécessaire ... 🙏

✅ Prérequis

Three-tier PKI
Generated with DALL-EUne infrastructure à trois niveaux de PKI (Three-tier PKI) comprend une Autorité de Certification Racine (CA) au sommet, des Autorités de Certification Intermédiaires au milieu, et des Entités Finales à la base. L'Autorité de Certification Racine délivre des certificats aux Autorités de Certification Intermédiaires, qui à leur tour émettent des certificats aux utilisateurs finaux ou aux dispositifs.
Cette structure renforce la sécurité en réduisant l'exposition de l'Autorité de Certification Racine et simplifie la gestion et la révocation des certificats, offrant une solution évolutive et flexible pour la sécurité numérique.

Pour renforcer la sécurité le système de gestion de certificats, il est recommandé de créer une Autorité de Certification Racine (AC Racine) hors ligne. Nous devons donc réaliser, au préalable, les étapes suivantes :

  • Générer l'Autorité de Certification Racine hors ligne : Cette approche minimise les risques de sécurité en isolant l'AC Racine du réseau.

  • Créer une Autorité de Certification Intermédiaire : Elle agit sous l'autorité de l'AC Racine et est utilisée pour émettre des certificats, permettant une gestion plus flexible et sécurisée.

  • Générer le certificat pour le serveur Vault depuis l'AC Intermédiaire : Cela assure une chaîne de confiance depuis l'AC Racine jusqu'aux certificats utilisateurs finaux, en passant par l'AC Intermédiaire.

En suivant la procédure décrite ici vous devriez obtenir les fichiers suivants qui seront utilisés dans le reste de cet article. Il s'agit là d'une proposition basé sur openssl, et vous pouvez utiliser la méthode qui vous convient pour parvenir au même résultat

1cd terraform/vault/cluster
2
3ls .tls/*.pem
4.tls/bundle.pem  .tls/ca-chain.pem  .tls/intermediate-ca-key.pem  .tls/intermediate-ca.pem  .tls/root-ca-key.pem  .tls/root-ca.pem  .tls/vault-key.pem  .tls/vault.pem

🏗️ Construire le cluster

Il existe plusieurs méthodes pour déployer un cluster Vault mais je n'ai pas trouvé celle qui me convenait, je l'ai donc construite en prenant les décisions suivantes:

  • Stockage intégré basé sur le protocole Raft, qui est particulièrement adapté aux systèmes distribués et garantit une résilience élevée. Voici un tableau illustrant la tolérance aux pannes en fonction de la taille du cluster :

    Cluster sizeFailure tolerance
    10
    31
    52
    73

    Par conséquent notre cluster Vault sera composé de 5 membres, ce qui nous permettra de tolérer la défaillance de 2 nœuds.

  • Stratégie de nœuds éphémères avec instances SPOT : L'architecture est constituée exclusivement d'instances SPOT pour une efficacité optimale en termes de coût. Ce groupe est configuré avec trois pools d'instances Spot distincts, chacun exploitant un type d'instance différent. Cette diversification stratégique vise à pallier toute défaillance potentielle liée à une pénurie spécifique de type d'instance SPOT, assurant ainsi une haute disponibilité et une continuité de service ininterrompue, tout en maximisant l'efficience des coûts.

  • Fonctionnalité de déverrouillage automatique de Vault (Unseal) : Cette fonction est essentielle compte tenu de la nature éphémère de nos nœuds. Elle permet de minimiser les temps d'arrêt et d'éliminer le besoin d'interventions manuelles pour le déverrouillage de Vault.

Cet article n'a pas pour but de décrire toutes les étapes qui sont disponibles dans la documentation du repo Github. Le fichier de variables Opentofu contient la configuration souhaitée.

 1name                  = "ogenki-vault"
 2leader_tls_servername = "vault.priv.cloud.ogenki.io"
 3domain_name           = "priv.cloud.ogenki.io"
 4env                   = "dev"
 5mode                  = "ha"
 6region                = "eu-west-3"
 7enable_ssm            = true
 8
 9# Use hardened AMI
10ami_owner = "xxx" # Compte AWS où se trouve l'AMI
11ami_filter = {
12  "name" = ["*hardened-ubuntu-*"]
13}

Après avoir exécuté l'ensemble des étapes, Vault peut être utilisé et nous obtenons un cluster constitué de 5 noeuds.

Vault login

🛠️ Configuration

Le déploiement d'une plateforme complète se fait par étapes distinctes car certaines opérations doivent être faites manuellement afin de garantir une sécurité optimale: La génération du certificat racine qui doit être conservé hors ligne et l'initialisation de Vault avec le token root initial.

Deploy sequence

Il faut bien entendu tous les composants réseaux afin d'y déployer des machines, puis le cluster Vault peut être installé et configuré avant de considérer l'ajout d'autres éléments d'infrastructure, qui dépendront probablement des informations sensibles stockées dans Vault.

La configuration de Vault est appliquée grâce au provider Terraform dont l'authentification se fait via un token généré depuis l'instance Vault. La proposition ici démontre comment configurer la PKI et autoriser les applications internes à interagir avec l'API de Vault et, en particulier, comment configurer Cert-Manager.

Il suffit donc de déclarer les variables propre à votre organisation

1domain_name      = "priv.cloud.ogenki.io"
2pki_common_name  = "Ogenki Vault Issuer"
3pki_country      = "France"
4pki_organization = "Ogenki"
5pki_domains = [
6  "cluster.local",
7  "priv.cloud.ogenki.io"
8]

Après avoir suivi la procédure, la PKI est configurée et il est alors possible de générer des certificats.

Vault Issuer

Installer la CA privée sur les machines

Contrairement aux PKI publiques, où les certificats sont automatiquement approuvés par les logiciels clients, dans une PKI privée, les certificats doivent être approuvés manuellement par les utilisateurs ou déployés sur tous les appareils par l'administrateur de domaine​

💾 Sauvegardes planifiées

Comme toute solution contenant des données, il est indispensable de les sauvegarder. Notons aussi la sensibilité de celles stockées dans Vault. Il nous faut donc une sauvegarde régulière dans lieu sécurisé.
La solution proposée ici est tout simplement une Cronjob. Elle utilise Crossplane pour construire les ressources AWS et se décompose comme suit:

  • Un bucket S3 où seront stockés les snapshots
  • Une polique de rétention pour ne conserver que les 30 dernières sauvegardes.
  • Le bucket est chiffré avec une clé KMS spécifique.
  • Un external-secret pour pouvoir récupérer les paramètres d'authentificatinon de l'Approle spécifique à la Cronjob.
  • Une Cronjob qui exécute le script disponible dans le repo et qui éffectue un snapshot tel que décrit dans la doc d'Hashicorp.
  • Un rôle IRSA qui donne les permissions au pod d'écrire les snapshots sur S3.

🚀 En pratique avec Gateway API!

L'objectif de cet article est de démontrer une utilisation concrète avec Gateway-API et, en fonction du protocole utilisé, plusieurs options sont possibles pour sécuriser les connexions avec du TLS. Nous pouvons notamment faire du Passthrough et faire en sorte que la terminaison TLS se fasse sur l'upstream (exposé directement par le pod).
En revanche pour notre cas d'usage, nous allons utiliser le cas le plus commun: HTTPS au niveau de la Gateway. Voici un exemple simple car il suffit uniquement d'indiquer le secret Kubernetes contenant le certificat

1listeners:
2- protocol: HTTPS
3  port: 443
4  tls:
5    mode: Terminate
6    certificateRefs:
7    - name: foobar-tls

Voyons cela en détail, car il y a certains éléments à préparer afin de pouvoir obtenir ces secrets 🔍.

☁️ Un certificat publique

Info
Cert-Manager est un outil open source permettant de gérer les certificats TLS dans Kubernetes.
Il s'agit, en fait, d'un opérateur Kubernetes qui est contrôlé par l'usage de CRDs (Custom Resources Definitions): il est en effet possible de générer des certificats en créant des resources de type certificate. Cert-manager se charge ensuite de vérifier qu'ils sont toujours valides et déclenche un renouvellement lorsque c'est nécessaire.
Il peut être intégré avec un nombre grandissant d'autorité de certifications comme Let's Encrypt, Venafi, Google, Vault ...

Dans la mise en place de cert-manager avec Let's Encrypt, on utilise un Issuer pour configurer la génération de certificats dans un namespace spécifique. Par contre, un ClusterIssuer étend cette capacité à tous les namespaces du cluster, offrant ainsi une solution plus globale et flexible pour la gestion des certificats.

security/base/cert-manager/le-clusterissuer-prod.yaml

 1apiVersion: cert-manager.io/v1
 2kind: ClusterIssuer
 3metadata:
 4  name: letsencrypt-prod
 5spec:
 6  acme:
 7    email: mymail@domain.tld
 8    server: https://acme-v02.api.letsencrypt.org/directory
 9    privateKeySecretRef:
10      name: ogenki-issuer-account-key
11    solvers:
12      - selector:
13          dnsZones:
14            - "cloud.ogenki.io"
15        dns01:
16          route53:
17            region: eu-west-3
  • Nous utilisons ici l'instance de prod de Let's Encrypt qui est soumise à certaines règles et il est recommandé de commencer vos tests sur l'instance de staging.
  • L'adresse email est utilisée pour recevoir des notifications, comme la nécessité de renouveler
  • Une clé ogenki-issuer-account-key est générée et est utilisée pour s'authentifier auprès du serveur ACME.
  • Le mécanisme qui permet de prouver la légitimité d'une demande de certificat est faite grâce à une résolution DNS.

A présent, comment pouvons-nous faire appel à ce ClusterIssuer depuis une resource Gateway-API?
Figurez-vous qu'il y a une intégration très simple par l'usage d'une annotation au niveau de la Gateway. Cette solution est expérimentale et requiert un paramètre spécifique lors du déploiment de cert-manager.

security/base/cert-manager/helmrelease.yaml

1apiVersion: helm.toolkit.fluxcd.io/v2beta2
2kind: HelmRelease
3metadata:
4  name: cert-manager
5  namespace: security
6spec:
7  values:
8...
9    featureGates: ExperimentalGatewayAPISupport=true

Il est aussi nécessaire de donner les permissions au contrôleur Cert-manager d'interagir avec Route53 pour pouvoir compléter le challenge DNS. Ici j'utilise une Composition Crossplane. (ℹ️ Si vous souhaitez creuser le sujet c'est par ici.)

Puis il faut ajouter l'annotation dans la Gateway et indiquer le secret cible.

infrastructure/base/gapi/platform-public-gateway.yaml

 1apiVersion: gateway.networking.k8s.io/v1beta1
 2kind: Gateway
 3metadata:
 4  name: platform-public
 5  annotations:
 6    cert-manager.io/cluster-issuer: letsencrypt-prod
 7spec:
 8  gatewayClassName: cilium
 9  listeners:
10    - name: http
11      hostname: "*.${domain_name}"
12...
13      tls:
14        mode: Terminate
15        certificateRefs:
16          - name: platform-public-tls

Lorsque la Gateway est créé, un certificat est généré. Ce certificat utilise le ClusterIssuer letsencrypt-prod indiqué ci-dessus.

 1kubectl describe certificate -n infrastructure platform-public-tls
 2Name:         platform-public-tls
 3Namespace:    infrastructure
 4API Version:  cert-manager.io/v1
 5Kind:         Certificate
 6...
 7Spec:
 8  Dns Names:
 9    *.cloud.ogenki.io
10  Issuer Ref:
11    Group:      cert-manager.io
12    Kind:       ClusterIssuer
13    Name:       letsencrypt-prod
14  Secret Name:  platform-public-tls
15  Usages:
16    digital signature
17    key encipherment
18Status:
19  Conditions:
20    Last Transition Time:  2024-01-24T20:43:26Z
21    Message:               Certificate is up to date and has not expired
22    Observed Generation:   1
23    Reason:                Ready
24    Status:                True
25    Type:                  Ready
26  Not After:               2024-04-23T19:43:24Z
27  Not Before:              2024-01-24T19:43:25Z
28  Renewal Time:            2024-03-24T19:43:24Z
29  Revision:                1

Enfin, au bout de quelques secondes, un secret Kubernetes est créé et contient le certificat. Il s'agit d'un secret de type TLS contenant les fichiers tls.crt tls.key et ca.crt

Le plugin view-cert

Les certificats générés par cert-manager sont stockés dans des secrets Kubernetes. Bien qu'il soit possible de les extraire à coup de commandes base64 et openssl. Pourquoi ne pas se simplifier la vie? Je suis un adepte de la ligne de commande et j'utilise pour ma part régulièrement le plugin view-cert qui permet d'afficher une synthèse des secrets de type tls.

 1kubectl view-cert -n infrastructure platform-public-tls
 2[
 3    {
 4        "SecretName": "platform-public-tls",
 5        "Namespace": "infrastructure",
 6        "Version": 3,
 7        "SerialNumber": "35f659ad03e437805fbf48111b74738efe3",
 8        "Issuer": "CN=R3,O=Let's Encrypt,C=US",
 9        "Validity": {
10            "NotBefore": "2024-01-28T09:41:35Z",
11            "NotAfter": "2024-04-27T09:41:34Z"
12        },
13        "Subject": "CN=*.cloud.ogenki.io",
14        "IsCA": false
15    }
16]

Il peut être installé en utilisant krew

1kubectl krew install view-cert

🏠 Un certificat privée

Pour un certificat privé avec Vault, nous devons aussi déclarer un ClusterIssuer mais sa définition différe légerement:

security/base/cert-manager/vault-clusterissuer.yaml

 1apiVersion: cert-manager.io/v1
 2kind: ClusterIssuer
 3metadata:
 4  name: vault
 5  namespace: security
 6spec:
 7  vault:
 8    server: https://vault.priv.cloud.ogenki.io:8200
 9    path: pki_private_issuer/sign/ogenki
10    caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0...
11    auth:
12      appRole:
13        path: approle
14        roleId: f8363d0f-b7db-9b08-67ab-8425ab527587
15        secretRef:
16          name: cert-manager-vault-approle
17          key: secretId
  • L'URL indiquée est celle du serveur Vault. Elle doit être accessible depuis les pods dans Kubernetes
  • Le path dans Vault fait partie de la phase de configuration de Vault. Il s'agit du rôle autorisé à généré des certificats.
  • Nous utilisons ici une authentification via un Approle.

Pour plus de détails sur l'ensemble des actions nécessaires à la configuration de Cert-Manager avec Vault, vous référer à cette procédure.

La principale différence avec la méthode utilisée pour Let's Encrypt réside dans le faut que le certificat doit être créé explicitement. En effet, la méthode précédente permettait de le faire automatiquement avec une annotation.

infrastructure/base/gapi/platform-private-gateway-certificate.yaml

 1apiVersion: cert-manager.io/v1
 2kind: Certificate
 3metadata:
 4  name: private-gateway-certificate
 5spec:
 6  secretName: private-gateway-tls
 7  duration: 2160h # 90d
 8  renewBefore: 360h # 15d
 9  commonName: private-gateway.priv.cloud.ogenki.io
10  dnsNames:
11    - gitops-${cluster_name}.priv.${domain_name}
12    - grafana-${cluster_name}.priv.${domain_name}
13    - harbor.priv.${domain_name}
14  issuerRef:
15    name: vault
16    kind: ClusterIssuer
17    group: cert-manager.io

Comme on peut le voir, ce certificat pourra être utilisé pour accéder aux applications weave-gitops, grafana et harbor. Il a une durée de validité de 90 jours et sera renouvelé automatiquement 15 jours avant son expiration.

Quelques secondes après la création de la resource certificate, un secret Kubernetes est généré.

 1kubectl describe certificates -n infrastructure private-gateway-certificate
 2Name:         private-gateway-certificate
 3Namespace:    infrastructure
 4API Version:  cert-manager.io/v1
 5Kind:         Certificate
 6...
 7Spec:
 8  Common Name:  private-gateway.priv.cloud.ogenki.io
 9  Dns Names:
10    gitops-mycluster-0.priv.cloud.ogenki.io
11    grafana-mycluster-0.priv.cloud.ogenki.io
12    harbor.priv.cloud.ogenki.io
13  Duration:  2160h0m0s
14  Issuer Ref:
15    Group:       cert-manager.io
16    Kind:        ClusterIssuer
17    Name:        vault
18  Renew Before:  360h0m0s
19  Secret Name:   private-gateway-tls
20Status:
21  Conditions:
22    Last Transition Time:  2024-01-27T19:54:57Z
23    Message:               Certificate is up to date and has not expired
24    Observed Generation:   1
25    Reason:                Ready
26    Status:                True
27    Type:                  Ready
28  Not After:               2024-04-26T19:54:57Z
29  Not Before:              2024-01-27T19:54:27Z
30  Renewal Time:            2024-04-11T19:54:57Z
31  Revision:                1
32Events:
33  Type    Reason     Age   From                                       Message
34  ----    ------     ----  ----                                       -------
35  Normal  Issuing    41m   cert-manager-certificates-trigger          Issuing certificate as Secret does not exist
36  Normal  Generated  41m   cert-manager-certificates-key-manager      Stored new private key in temporary Secret resource "private-gateway-certificate-jggkv"
37  Normal  Requested  41m   cert-manager-certificates-request-manager  Created new CertificateRequest resource "private-gateway-certificate-1"
38  Normal  Issuing    38m   cert-manager-certificates-issuing          The certificate has been successfully issued

Enfin, Il suffit d'utiliser ce secret dans la déclaration de la Gateway privée.

infrastructure/base/gapi/platform-private-gateway.yaml

 1apiVersion: gateway.networking.k8s.io/v1beta1
 2kind: Gateway
 3metadata:
 4  name: platform-private
 5spec:
 6  gatewayClassName: cilium
 7  listeners:
 8    - name: http
 9      hostname: "*.priv.${domain_name}"
10...
11      tls:
12        mode: Terminate
13        certificateRefs:
14          - name: private-gateway-tls

Nous pouvons vérifier l'autorité de certification en utilisant la commande curl:

1curl --verbose -k https://gitops-mycluster-0.priv.cloud.ogenki.io 2>&1 | grep 'issuer:'
2*  issuer: O=Ogenki; CN=Ogenki Vault Issuer

💭 Dernières remarques

❓ Qui n'a pas subit un incident lié au renouvellement d'un certificat?
❓ Comment obtenir un niveau de sécurité correspondant aux éxigences de l'entreprise?
❓ Comment peut-on se simplifier les tâches opérationnelles liées à la maintenance des certificats TLS?

Nous avons pu explorer une réponse concrète à ces questions dans cet article. L'automatisation mise en oeuvre grâce à Cert-manager permet de minimiser les tâches opérationnelles tout en améliorant le niveau de sécurité.

La mise en oeuvre avec Let's Encrypt et Gateway API est vraiment super simple! D'autre part le niveau de sécurité apporté par Vault pour les commmunications internes est vraiment à considérer. Cependant, il est vrai que cela nécessite la mise en oeuvre de plusieurs composants et une rigueur sans faille pour conserver un niveau de sécurité optimal.

Points d'attention pour la prod

Il est important de rappeler quelques recommandations et bonnes pratiques avant de considérer une mise en production. Afin que cet article reste lisible, certains points ne sont même pas été adressés mais il est primordial de les inclure dans votre stratégie:

  • Conservez le certificat racine hors ligne. En d'autres termes, il est impératif de le stocker sur un support non connecté pour le protéger de toute menace potentielle.
  • La révocation de la CA root ou intermédiaire n'a pas été évoqué. Ainsi que la mise à disposition d'une liste de révocation (Certificate Revocation List).
  • L'accès à l'API Vault doit être rigoureusement restreint à un réseau privé. Vous devriez jeter un coup d'oeil à mon article sur Tailscale.
  • Vous noterez aussi que je ne parle pas du tout d'authentification mais Il est essentiel de configurer un fournisseur d'identité dès le début et d'activer l'authentification multi-facteurs (MFA) pour renforcer la sécurité. Par ailleurs, il est conseillé de révoquer le token racine de Vault une fois l'authentification et les autorisations adéquates mises en place. Si nécessaire, le token peut être régénéré suivant la procédure disponible ici.
  • Par défaut le code proposé déploie des AMI (Images d'instances AWS) Ubuntu de Canonical. Il est conseillé d'en utiliser une dont la sécurité a été renforcée (Hardened AMI). J'ai construit la mienne en utilisant ce projet.
  • Afin de pouvoir initialiser Vault une commande doit être lancée sur l'instance ce qui justifie l'utilisation de SSM. Cependant il est conseillé de le désactiver lorsque la phase d'initialisation est terminée (enable_ssm: false dans les variables Opentofu)
  • Envoyez les logs d'audit vers un SIEM afin de pouvoir détecter des comportements suspects.
  • Alerter avant que les certificats n'arrivent à expiration. Vous pouvez, par exemple, utiliser cet exporter Prometheus opensourcé par les potes d'Enix 😉. Il s'agit là d'une sécurité supplémentaire sachant que l'architecture proposée rend le tout automatisé.
  • Accordez une attention particulière aux clés KMS: celle utilisé pour dévérouiller Vault, mais aussi celle qui permet de créer des snapshots. Elles sont vitalespour la restauration de vos sauvegardes.
  • "Une sauvegarde qui n'est pas vérifiée ne sert à rien": Il faut donc construire un workflow qui permettra de vérifier la consistence des données dans Vault. C'est peut être le sujet d'un autre article, stay tuned!
  • Organisez périodiquement des exercices de reprise après sinistre (PRA) pour garantir votre capacité à reconstruire l'ensemble du système à partir de zéro, en vous assurant de disposer de toute la documentation et des outils nécessaires.

🔖 References

Github Issues

Blog posts

Documentation Hashicorp

Traductions: