Pattern FROM scratch : construire une image Docker tier Platine

Cet article documente le pattern commun utilise pour construire les 7 images Docker hardened du homelab. Chaque image suit la meme architecture multi-stage et les memes principes de securite.

Pourquoi FROM scratch

Une image FROM scratch ne contient aucun OS : pas de shell, pas de package manager, pas de coreutils. Seuls les binaires strictement necessaires au service sont presents. Cette approche reduit la surface d attaque au minimum absolu.

Comparaison avec les alternatives :

Base Taille typique Shell Outils Surface d attaque
Ubuntu/Debian 150-300 MB bash, sh, dash apt, curl, wget... Large
Alpine 5-10 MB sh (busybox) apk Moyenne
Distroless 20-50 MB Non Aucun Reduite
FROM scratch 15-35 MB Non Aucun Minimale

Distroless (Google) est un downgrade par rapport a FROM scratch car il embarque des packages supplementaires. FROM scratch = zero dependance externe.

Architecture multi-stage

flowchart TB
    subgraph s1["Stage 1 : builder"]
        compile["Compile from source<br/>avec hardening flags"]
        strip["strip binaires"]
    end
    subgraph s2["Stage 2 : gobuilder"]
        gobuild["CGO_ENABLED=0<br/>go build init.go"]
    end
    subgraph s3["Stage 3 : prep"]
        libs["Runtime libs (APK)"]
        tini["tini-static"]
        user["adduser non-root"]
        setcap["setcap capabilities"]
    end
    subgraph s4["Stage 4 : FROM scratch"]
        copy["COPY --link binaires"]
        setup["RUN init --setup-dirs"]
        final["USER + HEALTHCHECK<br/>+ ENTRYPOINT"]
    end
    s1 --> s3
    s2 --> s4
    s3 --> s4
    compile --> strip
    libs --> setcap
    copy --> setup --> final

Stage 1 : builder

Compile le service from source sur Alpine avec les flags de hardening :

CFLAGS  = -O2 -fstack-protector-strong -fstack-clash-protection
          -fPIE -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security
LDFLAGS = -Wl,-z,relro,-z,now,-z,noexecstack -pie
Flag Protection
Full RELRO GOT read-only apres link
PIE ASLR complet (Position Independent Executable)
SSP Canaries sur les fonctions avec arrays/pointeurs
Stack Clash Guard pages contre stack clash attacks
FORTIFY_SOURCE Detection buffer overflow a la compilation
noexecstack Stack non-executable

Les binaires sont ensuite strippes pour reduire la taille.

Stage 2 : gobuilder

Compile le binaire Go init en mode statique :

CGO_ENABLED=0 GOOS=linux go build -ldflags='-s -w' -trimpath -o /init .

Ce binaire unique remplace 3 scripts shell :

  • Entrypoint (pre-checks, config validation, exec)
  • Healthcheck (verification applicative)
  • Setup-dirs (creation des repertoires au build time)

Stage 3 : prep

Assemble le filesystem runtime sur Alpine :

  • Installe les libs dynamiques necessaires (pas les -dev)
  • Ajoute tini-static (PID 1)
  • Cree l utilisateur non-root avec un UID explicite
  • Applique setcap sur le binaire principal

Stage 4 : FROM scratch

FROM scratch
COPY --link --from=prep /etc/passwd /etc/group /etc/
COPY --link --from=prep /lib/ld-musl-x86_64.so.1 /lib/
COPY --link --from=prep /usr/lib/ /usr/lib/
COPY --link --from=prep /usr/sbin/named /usr/sbin/
COPY --link --from=prep /sbin/tini-static /sbin/tini
COPY --link --from=gobuilder /init /usr/local/bin/init
RUN ["/usr/local/bin/init", "--setup-dirs"]
USER 5300:5300
HEALTHCHECK CMD ["/usr/local/bin/init", "--healthcheck"]
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/init"]

COPY --link cree des layers independantes du parent pour un meilleur cache Docker.

Go init binary

Chaque image a son propre init.go avec 3 modes :

flowchart LR
    args{"os.Args?"}
    args -->|"--setup-dirs"| setup["Cree les repertoires<br/>avec chown UID:GID"]
    args -->|"--healthcheck"| health["Verification applicative<br/>exit 0 ou 1"]
    args -->|"default"| entry["Pre-checks<br/>Config validation<br/>syscall.Exec"]

Strategies de healthcheck par image

Image Methode Detail
bind9 DNS UDP query CH TXT version.bind vers 127.0.0.1:53
squid HTTP cache_object GET cache_object://localhost/info
nginx-waf HTTP GET /healthz Host: localhost
clamav TCP PING/PONG Envoie PING, attend PONG
c-icap ICAP OPTIONS OPTIONS icap://localhost/squidclamav
suricata PID file + socket Fallback unix socket si PID absent
php-fpm FastCGI PING/PONG Protocole FastCGI en Go pur

Entrypoint : config validation avant exec

Chaque init valide la configuration avant de lancer le service :

  • bind9 : named-checkconf (fatal)
  • squid : squid -k parse (fatal)
  • nginx : nginx -t (fatal)
  • suricata : suricata -T (warning, non-fatal car les rules inconnues ne bloquent pas)

Si la validation echoue, le container ne demarre pas. Pas de service en etat degrade.

UIDs et capabilities

Image UID Capability Raison
bind9 5300 NET_BIND_SERVICE Port 53
squid 3128 - Port > 1024
clamav 4000 - Port > 1024
c-icap 4100 - Port > 1024
suricata 8000 NET_ADMIN, SYS_NICE NFQUEUE
nginx-waf 1999 - Port > 1024 (8080)
php-fpm 1999 - Port > 1024 (9000)

Les capabilities sont posees via setcap sur le binaire dans le stage prep. Le container VyOS ajoute la capability correspondante (set container name X capability net-bind-service).

Transfer vers VyOS

VyOS n a pas d acces Internet sortant. Le pipeline de transfer :

flowchart LR
    build["docker build"] --> save["docker save<br/>image.tar"]
    save --> scp["scp vers VyOS"]
    scp --> load["podman load"]
    load --> config["modifier config.boot<br/>set container image"]
    config --> restart["restart container"]

Apres le transfer, la bascule se fait via la configuration VyOS native (pas de sudo, pas de podman directement).

Les 8 images

Image Version Taille Repo
bind9-hardened 9.20.24 19 MB GitHub
squid-hardened 7.5 ~20 MB GitHub
c-icap-hardened 0.6.4 ~15 MB GitHub
clamav-hardened 1.4.2 ~30 MB GitHub
suricata-hardened 8.0.5 ~25 MB GitHub
nginx-waf-hardened 1.30.3 ~22 MB GitHub
php-fpm-hardened 8.5.3 ~35 MB GitHub
varnish-hardened 7.7.3 ~18 MB GitHub

Chaque image a ses propres articles detailles dans la categorie Images Hardened.


Liens

Articles par image

Articles connexes


Commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.