forked from quentin/quentin.dufour.io
231 lines
7.3 KiB
Markdown
231 lines
7.3 KiB
Markdown
---
|
||
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 :
|
||
|
||
```nix
|
||
{
|
||
description = "TLS Static Golang";
|
||
|
||
inputs = {
|
||
nixpkgs.url = "github:nixos/nixpkgs/master";
|
||
gomod2nix.url = "github:tweag/gomod2nix/master";
|
||
};
|
||
|
||
outputs = { self, nixpkgs, gomod2nix }:
|
||
let
|
||
# On configure "le dépôt Nix"
|
||
pkgs = import nixpkgs {
|
||
system = "x86_64-linux";
|
||
overlays = [
|
||
(import "${gomod2nix}/overlay.nix")
|
||
];
|
||
};
|
||
|
||
# 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.
|
||
On va donc injecter là aussi ces certificats dans le conteneur, et pour ce faire on réalise la modification suivante :
|
||
|
||
```diff
|
||
--- flake.nix.old 2024-06-09 16:43:21.169415893 +0200
|
||
+++ flake.nix 2024-06-09 16:41:54.325022233 +0200
|
||
@@ -28,7 +28,10 @@
|
||
# 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`.
|
||
|
||
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.
|