Sécuriser vos applications Kubernetes avec Cert-Manager et Scaleway DNS
Kubernetes Kapsule est le service de Kubernetes managé de Scaleway. Nous avons déjà traité la présentation du service dans un précédent article. Nous avions été assez élogieux, mettant particulièrement en avant le travail effectué sur le provider Terraform permettant de déployer et de manager, entièrement en infrastructure as code, nos clusters Kubernetes.
Nous avons aussi abordé l’utilisation des PodSecurityPolicy sur Kapsule, nous vous laissons (re)découvrir cet article si vous le souhaitez.
Dans un dernier article, nous vous faisions découvrir (ou pas) le service
External-DNS permettant de
synchroniser l’état de nos Services
et Ingress
avec
un service DNS. Suite à la sortie du DNS as a Service chez Scaleway,
External-DNS devenait compatible avec ce dernier et s’intégrait parfaitement
avec Kubernetes Kapsule.
Grâce à External-DNS, nous sommes capables de déployer une application
quasiment de bout en bout, celle ci est déployée en haute disponibilité avec
notre couple Deployment
/Service
et est exposée via un nom de domaine avec le
couple Ingress/External-DNS.
Mais il manque quelque chose : la sécurisation de votre application avec TLS.
Cert-Manager
Cert-Manager est un gestionnaire natif de certificats pour Kubernetes. Il permet la génération de certificats depuis différentes sources comme Let’s Encrypt, HashiCorp Vault ou bien un auto-signé. Le contrôleur de cert-manager se chargera de renouveller le certificat afin d’éviter son expiration.
Afin de ne pas permettre à n’importe qui de demander un certificat pour n’importe quel CommonName, cert-manager utilise le protocole ACME afin d’effectuer un challenge permettant de prouver votre identité. Deux grands mécanismes existent pour valider votre possession du nom de domaine pour lequel vous désirez obtenir un certificat valide : HTTP-01 et DNS-01.
https://letsencrypt.org/fr/docs/challenge-types/
Long story short, le challenge HTTP-01 vous demande de prouver être capable d’héberger du contenu à l’endroit où pointe votre nom de domaine et le challenge DNS-01 vous demande de prouver que vous posséder le nom de domaine en y créant un enregistrement DNS.
HTTP-01 marche à peu près partout, il n’y a pas de dépendance à un tiers (à moins que votre fournisseur bloque le port 80, mais qui fait ça ?). DNS-01 demande de posséder un service DNS piloté par une API afin de créer les enregistrements qui permettront de réussir le challenge. Et ça tombe bien, Scaleway a ça et depuis peu propose un webhook pour Kubernetes permettant d’utiliser leur service DNS avec cert-manager.
Voici donc la pièce manquante du puzzle pour déployer, réellement de bout en bout, notre application.
Pré-requis
- un cluster Kubernetes Kapsule
- Helm v3 sur votre PC
- un DNS configuré sur Kapsule (nous utiliserons
scw.particule.cloud
ici)
Pour l’installation de Kapsule vous pouvez vous reporter à notre première article.
Installation de cert-manager
Là c’est la partie facile. Il suffit d’appliquer le yaml officiel.
$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.0.4/cert-manager.yaml
Attendez quelques instants que les CRD cert-manager soient correctement installées avant de passer à la suite.
Installation du webhook Scaleway
$ git clone https://github.com/scaleway/cert-manager-webhook-scaleway.git
$ cd cert-manager-webhook-scaleway
$ helm install scaleway-webhook deploy/scaleway-webhook
L’étape suivante est de créer un Issuer
, il s’agit du composant qui va faire la
demande de certificat à Let’s Encrypt. Afin de permettre à lIssuer
de valider
le challenge DNS, il va lui falloir un accès au DNS de Scaleway et donc un
accès à vos crédentials. Stockons les dans un Secret :
Votre Secret
, l'Issuer
et le webhook doivent être placés dans le même
namespace.
---
apiVersion: v1
stringData:
SCW_ACCESS_KEY: <YOUR-SCALEWAY-ACCESS-KEY>
SCW_SECRET_KEY: <YOUR-SCALEWAY-SECRET-KEY>
kind: Secret
metadata:
name: scaleway-secret
type: Opaque
Grâce au paramètre stringData
, vous n’avez pas besoin d’encoder vos données
en base64.
On crée maintenant l'Issuer
.
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: scaleway
spec:
acme:
email: romain@particule.io
server: https://acme-staging-v02.api.letsencrypt.org/directory
# for production use this URL instead
# server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: scaleway-acme-secret
solvers:
- dns01:
webhook:
groupName: acme.scaleway.com
solverName: scaleway
config:
accessKeySecretRef:
key: SCW_ACCESS_KEY
name: scaleway-secret
secretKeySecretRef:
key: SCW_SECRET_KEY
name: scaleway-secret
Nous sommes maintenant prêt à demander un certificat à Let’s Encrypt.
Application de test
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: helloworld
spec:
replicas: 1
selector:
matchLabels:
app: helloworld
template:
metadata:
labels:
app: helloworld
spec:
containers:
- name: helloworld
image: particule/helloworld
ports:
- name: web
containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: helloworld
spec:
ports:
- port: 80
selector:
app: helloworld
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: helloworld
annotations:
cert-manager.io/issuer: scaleway
kubernetes.io/tls-acme: "true"
spec:
rules:
- host: helloworld.scw.particule.cloud
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: helloworld
port:
number: 80
tls:
- hosts:
- helloworld.scw.particule.cloud
secretName: helloworld.scw.particule.cloud-cert
Le Deployment
et le Service
sont très classiques, rien à signaler de ce
coté.
En revanche l'Ingress
a subit quelques ajouts. Premièrement, il utilise
networking.k8s.io/v1
et la syntaxe de spec.rules
change légèrement. Mais
surtout nous avons des changements au niveau des annotations et au niveau de la
spec.tls
.
Deux annotations sont ajoutées, la première défini l’issuer que nous allons utiliser, la deuxième active le protocole ACME.
Au niveau du TLS, on défini le Secret
dans lequel sera stocké le certificat
pour notre Ingress
.
On applique et on va surveiller la création de notre certificat :
$ kubectl get certificates -w
La création prend un peu de temps (~2min), n’hésitez pas à regarder les events de votre cluster pour voir les erreurs arivées ^^
$ kubectl get events
Vous devriez à un moment obtenir votre certificat.
$ kubectl get events
0s Normal Issuing certificate/helloworld.scw.particule.cloud-cert The certificate has been successfully issued
$ kubectl get certs
NAME READY SECRET AGE
helloworld.scw.particule.cloud-cert True helloworld.scw.particule.cloud-cert 8m
Et on peut effectivement voir notre certificat :
$ curl https://helloworld.scw.particule.cloud -vvvv -k
* Trying 212.47.236.1:443...
* TCP_NODELAY set
* Connected to helloworld.scw.particule.cloud (212.47.236.1) port 443 (#0)
[...]
* Server certificate:
* subject: CN=helloworld.scw.particule.cloud
* start date: Nov 21 16:31:19 2020 GMT
* expire date: Feb 19 16:31:19 2021 GMT
* issuer: CN=Fake LE Intermediate X1
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x5621f56a1e40)
> GET / HTTP/2
> Host: helloworld.scw.particule.cloud
> user-agent: curl/7.68.0
> accept: */*
[...]
> strict-transport-security: max-age=15724800; includeSubDomains
<html>
<head>
ClusterIssuer
Comme nous avons utilisé un Issuer
dans le namespace default, seuls les
Ingress
de ce namespace pourront demander un certificat. Afin de permettre ce
mécanisme à l’ensemble du cluster, nous allons utiliser un ClusterIssuer
. Il
s’agit de la même ressource mais cluster-wide (comme la différence entre un
Role
et un ClusterRole
).
Afin que cela puisse fonctionner, nous allons devoir modifier plusieurs choses.
Tout comme l'Issuer
, le ClusterIssuer
va définir le webhook à utiliser pour
résoudre le challenge DNS ainsi que les crédentials nécessaires.
Dans notre exemple précédent l'Issuer
référençait un Secret
dans son
propre namespace mais dans le cas d’un ClusterIssuer
celui ci le référence
dans le namespace de cert-manager. Si on souhaite que le webhook puisse
effectivement récupérer ce secret, nous avons deux choix :
- installer le webhook et le
Secret
dans le namespace cert-manager - installer le webhook dans un namespace dédié, le
Secret
dans le namespace de cert-manager et modifier les RBAC du helm chart afin que le webhook ait accès auxSecrets
du namespace cert-manager
Question de simplicité, nous allons utiliser la première solution ^^
La ressource Issuer
et ClusterIssuer
est identique en tous points, vous
n’avez qu’à modifier le Kind et le tour est joué. Pour le webhook et le
Secret
vous n’avez qu’à spécifier -n cert-manager
au déploiement.
À part ça rien ne change mais vous obtiendrez un comportement “multi-tenant” où plusieurs namespaces pourront utiliser l’issuer sans configuration et sans avoir accès aux crédentials Scaleway.
External-DNS
Pour compléter tout ceci, nous pouvons activer External-DNS afin que le nom de domaine pour lequel nous avons crée notre certificat possède bien un enregistrement.
Vous pouvez reprendre notre précédent article sur le sujet.
Ou alors, vous pouvez utiliser le module Terraform écrit par Particule vous permettant de déployer External-DNS très simplement. Il vous suffit d’ajouter ce code :
module "addons" {
source = "particuleio/addons/kubernetes//modules/scaleway"
version = "~> 1.0"
external-dns = {
enabled = true
scw_access_key = <YOUR-SCALEWAY-ACCESS-KEY
scw_secret_key = <YOUR-SCALEWAY-SECRET-KEY
scw_default_organization_id = <YOUR-SCALEWAY-ORGANIZATION-ID>
}
}
Nous reviendrons bientôt sur ce module Terraform et sur le travail que nous effectuons pour rendre le déploiement de Kubernetes Kapsule encore plus efficace et simple.
Conclusion
Un gros merci aux équipes de Scaleway pour avoir développé ce webhook, une nouvelle corde à l’arc de Kubernetes Kapsule lui permettant de rivaliser avec d’autres offres Kubernetes managées.
Romain Guichard, CEO & co-founder