quentin.dufour.io/_posts/2024-06-09-cacerts.md

228 lines
7.2 KiB
Markdown
Raw Normal View History

2024-06-09 14:48:16 +00:00
---
layout: post
title: TLS dans un conteneur statique
date: 2024-06-09
status: published
sitemap: true
category: developpement
description: Comment gérer TLS dans un conteneur statique
---
Cet article est motivé par un problème rencontré sur l'interface Guichet de Deuxfleurs.
Au moment d'intéragir avec l'API S3, on a cette erreur suivante :
```
Impossible d'effectuer la modification.
Put "https://garage.deuxfleurs.fr/bottin-pictures/d1e3607f-4b9c-45fa-9e11-ddf8eef48676-thumb": tls: failed to verify certificate: x509: certificate signed by unknown authority
```
Pour comprendre le problème, partons de cet exemple basique en Go :
```go
package main
import (
"net/http"
"log"
)
func main() {
_, err := http.Get("https://deuxfleurs.fr")
if err != nil {
log.Fatal(err)
}
log.Println("Success")
}
```
Ce bloc de code fait une requête HTTPS vers deuxfleurs.fr et log l'erreur si il y en a une, sinon il affiche `Success`.
On va le compiler en statique pour ne pas dépendre de la lib C locale :
```bash
CGO_ENABLED=0 go build main.go
```
Ce qui nous donne bien un executable statique :
```
$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=ctUIsYsrR2BtpR58vqRU/TI93T6hZlDxMBNqsplsv/QYD-xJaEDyWB0QaX6tSS/cDyvoEdvE3kZpdq8yCs3, with debug_info, not stripped
```
Si on le fait tourner en local, tout se passe bien :
```
2024/06/09 15:54:59 Success
```
Maintenant, puisqu'on nous avons un binaire statique qui ne dépend de rien, on va vouloir créer un conteneur Docker qui ne contient que ce fichier (alors que souvent on embarque une distribution de base comme Debian ou Alpine). Pour se faire, on écrit un Dockerfile avec deux étapes : une qui a les outils pour le build, et une qui ne contient que notre binaire :
```Dockerfile
FROM golang as builder
COPY main.go .
RUN CGO_ENABLED=0 go build -o /main main.go
FROM scratch
COPY --from=builder /main /main
CMD [ "/main" ]
```
On construit & lance le conteneur et... on reproduit l'erreur !
```
$ docker build -t tls-static-go .
$ docker run --rm -it tls-static-go
2024/06/09 14:02:37 Get "https://deuxfleurs.fr": tls: failed to verify certificate: x509: certificate signed by unknown authority
```
Pour comprendre la différence de comportement entre l'intérieur du conteneur et l'extérieur, on peut faire appel à `strace` et voir les fichiers que notre binaire essaie d'ouvrir :
```bash
$ strace -fe openat ./main
...
[pid 9066] openat(AT_FDCWD, "/etc/ssl/certs/ca-certificates.crt", O_RDONLY|O_CLOEXEC) = 7
[pid 9066] openat(AT_FDCWD, "/etc/ssl/certs", O_RDONLY|O_CLOEXEC) = 7
[pid 9066] openat(AT_FDCWD, "/etc/ssl/certs/ca-bundle.crt", O_RDONLY|O_CLOEXEC) = 7
...
```
En général ce fichier est fourni par les distributions, qui à ma connaissance majoritairement se basent sur le travail de Mozilla.
On peut en lire plus à propos de la gestion de ces racines de confiance sur la page dédiée du wiki de Mozilla : [CA/FAQ](https://wiki.mozilla.org/CA/FAQ)
Une façon de faire est donc de copier le fichier de notre builder dans notre conteneur final :
```diff
--- Dockerfile.old 2024-06-09 16:13:18.457415988 +0200
+++ Dockerfile 2024-06-09 16:12:59.494182932 +0200
@@ -4,5 +4,6 @@
FROM scratch
COPY --from=builder /main /main
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
CMD [ "/main" ]
```
On rebuild, on relance et... ça marche !
```
$ docker build -t tls-static-go-2 .
$ docker run --rm -it tls-static-go-2
2024/06/09 14:14:30 Success
```
Pour visualiser le contenu d'une image docker, on peut utiliser l'outil [dive](https://github.com/wagoodman/dive) :
![Dive](/assets/images/posts/dive.png)
À gauche, puis à droite, les commandes sont :
```bash
dive tls-static-go
dive tls-static-go-2
```
On voit bien l'ajout du chemin `/etc/ssl/certs/ca-certificates.crt` à droite.
Maintenant, le problème avec les `Dockerfile`, c'est que ce ne sont pas du tout des builds reproductibles ni précis : on ne sait pas quelles versions ou dépendances on embarque. Donc on veut plutôt construire nos conteneurs avec NixOS pour plus de contrôle.
Pour faciliter le packaging Nix, on va générer un fichier `go.mod` :
```bash
go mod init tls-static-go
go mod tidy
```
On peut ensuite créer un fichier `flake.nix` qui est notre équivalent mais plus précis de notre Dockerfile :
2024-06-09 14:48:16 +00:00
```nix
{
description = "TLS Static Golang";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/master";
};
2024-06-10 07:44:21 +00:00
outputs = { self, nixpkgs }:
2024-06-09 14:48:16 +00:00
let
# On configure "le dépôt Nix"
pkgs = import nixpkgs {
system = "x86_64-linux";
};
# On utilise le builder Go intégré à Nix pour construire notre app
tls-static = pkgs.buildGoModule {
pname = "tls-static";
version = "0.1.0";
src = ./.;
vendorHash = null;
CGO_ENABLED = 0;
};
# On construit une image Docker qui ne contient que l'app qu'on a build.
container = pkgs.dockerTools.buildImage {
name = "superboum/tls-static-go";
copyToRoot = tls-static;
config = {
Cmd = [ "/bin/tls-static" ];
};
};
in
{
# Par défaut, sous Linux amd64, on construit le conteneur
packages.x86_64-linux.default = container;
};
}
```
Et ensuite on peut construire / charger / lancer le conteneur Docker :
```
$ nix build
$ docker load <result
Loaded image: superboum/tls-static-go:zvsb5pwz25irhr90x10kpfhgsph5is1s
$ docker run --rm -it superboum/tls-static-go:zvsb5pwz25irhr90x10kpfhgsph5is1s
2024/06/09 14:37:57 Get "https://deuxfleurs.fr": tls: failed to verify certificate: x509: certificate signed by unknown authority
```
Et de nouveau la même erreur, on jette un coup d'oeil avec dive :
![Dive 2](/assets/images/posts/dive-2.png)
On voit qu'on a quelques dépendances nix de tirées (à propos des fuseaux horaires par exemple) mais rien lié aux certificats.
2024-06-09 14:48:16 +00:00
On va donc injecter là aussi ces certificats dans le conteneur, et pour ce faire on réalise la modification suivante :
```diff
2024-06-10 07:44:21 +00:00
--- flake.nix.old 2024-06-10 09:42:48.871184290 +0200
+++ flake.nix 2024-06-10 09:41:48.011959988 +0200
@@ -24,7 +24,10 @@
2024-06-09 14:48:16 +00:00
# On construit une image Docker qui ne contient que l'app qu'on a build.
container = pkgs.dockerTools.buildImage {
name = "superboum/tls-static-go";
- copyToRoot = tls-static;
+ copyToRoot = pkgs.buildEnv {
+ name = "tls-static-env";
+ paths = [ tls-static pkgs.cacert ];
+ };
config = {
Cmd = [ "/bin/tls-static" ];
};
```
Autrement dit, on a créé notre système de fichiers racine en fusionnant le contenu de notre build `tls-static` avec celui du paquet NixOS `cacert`.
2024-06-09 14:48:16 +00:00
On peut ensuite rebuild / load / run le conteneur avec... succès !
```
$ nix build
$ docker load <result
Loaded image: superboum/tls-static-go:02bmar6x0q1gi8b6j5ys6j1l84mn5vwh
$ docker run --rm -it superboum/tls-static-go:02bmar6x0q1gi8b6j5ys6j1l84mn5vwh
2024/06/09 14:45:06 Success
```
Quant à Dive, on voit bien que le bundle de certificat a été correctement injecté au bon endroit :
![Dive 3](/assets/images/posts/dive-3.png)
Et voilà, vous avez une image Docker fonctionnelle, minimaliste, reproductible, avec des dépendances correctement déclarées, et maintenable.