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
- BIND9 hardened
- Squid + c-icap + ClamAV
- WAF nginx ModSecurity
- Suricata IPS
- PHP-FPM WordPress
- Varnish Cache
Laisser un commentaire