Make the Dockerfile actually build the apt NIF

9049eaf · Gabriel Morell · 2026-09-12 02:48

11 files +68 -28
Message
{commit_body(@commit)}

Files changed

modified .dockerignore
+6 −0
@@ -39,6 +39,12 @@
39 39 # Generated on crash by the VM
40 40 erl_crash.dump
41 41
42 +# Rust build artifacts for the NIF crates. `COPY native native` would
43 +# otherwise drag well over a gigabyte of host-built output into the
44 +# context and into the builder layer, where cargo ignores it anyway —
45 +# it rebuilds against the image's toolchain and glibc.
46 +/native/*/target/
47 +
42 48 # Static artifacts - These should be fetched and built inside the Docker image
43 49 # https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Release.html#module-docker
44 50 /assets/node_modules/
modified Dockerfile
+8 −2
@@ -24,11 +24,17 @@ FROM ${BUILDER_IMAGE} AS builder
24 24
25 25 # install build dependencies.
26 26 # * build-essential, git — Elixir / mix build chain
27 −# * pkg-config, libssl-dev — gix transitively links against system openssl
27 +# * pkg-config, libssl-dev — gix transitively links against system openssl,
28 +# and gitgud_apt's sequoia-openpgp uses its OpenSSL crypto backend
29 +# * libclang-dev — `openssl-sys` generates its bindings with bindgen,
30 +# which dlopens libclang. Build-stage only: the final image never
31 +# sees it, and it is the one package the apt signing NIF adds
32 +# anywhere. Every sequoia backend that binds C needs this; only the
33 +# pure-Rust one avoids it, and that one is variable-time.
28 34 # * curl, ca-certificates — needed to bootstrap rustup
29 35 RUN apt-get update \
30 36 && apt-get install -y --no-install-recommends \
31 − build-essential git pkg-config libssl-dev curl ca-certificates \
37 + build-essential git pkg-config libssl-dev libclang-dev curl ca-certificates \
32 38 && rm -rf /var/lib/apt/lists/*
33 39
34 40 # Rust via rustup, pinned. apt's rust on trixie is 1.85, but recent
modified README.md
+20 −8
@@ -469,14 +469,26 @@ Rustler, and also carries xz and zstd, since a `.deb`'s control member
469 469 is `control.tar.xz` on anything since squeeze and `:erl_tar` knows only
470 470 gzip.
471 471
472 −Its crypto backend is chosen so the NIF adds **no package to either
473 −container image**: sequoia's default Nettle backend would pull
474 −`libnettle` and `libgmp` into the runtime image, and the pure-Rust
475 −backend is gated behind `allow-variable-time-crypto`, which is the
476 −wrong trade for code holding a private key. The OpenSSL backend links
477 −`libssl`/`libcrypto`, which the builder already installs for gix and
478 −the runner already installs for the Erlang runtime. The compression
479 −crates are pure Rust for the same reason.
472 +Its crypto backend is sequoia's **OpenSSL** one, which costs exactly
473 +one package in the builder image and nothing in the runtime image:
474 +
475 +| backend | builder needs | runtime needs | crypto |
476 +|---|---|---|---|
477 +| `crypto-openssl` | `libclang-dev` (`libssl-dev` already there for gix) | `libssl` — from `openssl`, named in the Dockerfile | constant-time, audited |
478 +| `crypto-nettle` (default) | `libclang-dev`, `nettle-dev`, `libgmp-dev` | libnettle/libgmp — present only transitively, via `git` | constant-time, audited |
479 +| `crypto-rust` | nothing | nothing | **variable-time**, gated behind `allow-variable-time-crypto` |
480 +
481 +`libclang-dev` is there because `openssl-sys` generates its bindings
482 +with bindgen, which dlopens libclang. Every sequoia backend that binds
483 +C needs it; only the pure-Rust one avoids it, and that one is the
484 +wrong trade for code holding a signing key. The builder is a separate
485 +stage, so the runtime image never sees it.
486 +
487 +This table is measured, not reasoned: the first version of this NIF
488 +built fine locally and then failed in the builder, because the
489 +developer's machine happened to have clang installed. `podman build -f
490 +Dockerfile .` is the only thing that settles it. The compression
491 +crates are pure Rust, so they cost nothing either way.
480 492
481 493 ## What's not done
482 494
modified lib/git_gud/packages/apt/native.ex
+21 −10
@@ -16,16 +16,27 @@ defmodule GitGud.Packages.Apt.Native do
16 16
17 17 ## Why these backends
18 18
19 − `sequoia-openpgp` defaults to Nettle, which would put `libnettle`
20 − and `libgmp` in the runtime image. This project ships k8s manifests;
21 − a NIF that changes the image's package list is a deployment change
22 − wearing a dependency's clothes. The pure-Rust backend needs no
23 − system libraries at all, but sequoia gates it behind
24 − `allow-variable-time-crypto`, and this code holds a private key. The
25 − OpenSSL backend links libssl and libcrypto, which are already in
26 − both images — the builder installs `libssl-dev` for gix and the
27 − runner installs `openssl` for the Erlang runtime — and is
28 − constant-time. So: OpenSSL, and no image change at all.
19 + This project ships k8s manifests, so a NIF that changes an image's
20 + package list is a deployment change wearing a dependency's clothes.
21 + The cost was measured by building the Dockerfile, not reasoned about:
22 +
23 + crypto-openssl builder: libclang-dev runtime: — (libssl already named)
24 + crypto-nettle builder: libclang-dev, runtime: — (but libnettle/libgmp
25 + nettle-dev, are present only as a
26 + libgmp-dev transitive dep of git)
27 + crypto-rust builder: — runtime: — variable-time
28 +
29 + `libclang-dev` is unavoidable for any backend that binds C:
30 + `openssl-sys` generates its bindings with bindgen, which dlopens
31 + libclang. Only the pure-Rust backend escapes it, and sequoia gates
32 + that behind `allow-variable-time-crypto` — the wrong trade for code
33 + holding a signing key.
34 +
35 + So: OpenSSL. One package, in the build stage only, for constant-time
36 + audited crypto. The runtime image is unchanged.
37 +
38 + The compression crates (`lzma-rs`, `ruzstd`) are pure Rust and cost
39 + nothing either way.
29 40
30 41 The compression crates (`lzma-rs`, `ruzstd`) are pure Rust for the
31 42 same reason: no new system libraries.
modified native/gitgud_apt/Cargo.toml
+13 −8
@@ -18,19 +18,24 @@ rustler = "0.34"
18 18 #
19 19 # Backend choice, which is the load-bearing decision here:
20 20 #
21 −# * `crypto-nettle` is the default and pulls libnettle + libgmp into
22 −# the *runtime* image. This project ships k8s manifests, and its
23 −# runner image installs no such packages — a NIF that dragged them
24 −# in would be a deployment change disguised as a dependency.
21 +# * `crypto-nettle` is the default and needs nettle-dev + libgmp-dev
22 +# in the *builder* image on top of libclang-dev, and the builder
23 +# installs none of them. This project ships k8s manifests, and a
24 +# NIF that changes an image's package list is a deployment change
25 +# disguised as a dependency. (The runtime image does carry
26 +# libnettle/libgmp already, but only as a transitive dependency of
27 +# git — not something to hang signing on.)
25 28 # * `crypto-rust` builds with no system libraries at all, but sequoia
26 29 # gates it behind `allow-experimental-crypto` and
27 30 # `allow-variable-time-crypto`. This code holds a private key; a
28 31 # variable-time implementation is not the right place to save a
29 32 # package.
30 −# * `crypto-openssl` links libssl/libcrypto, which are already in
31 −# both images — the builder installs libssl-dev for gix, and the
32 −# runner installs openssl for the Erlang runtime. No new packages
33 −# in either, and constant-time audited crypto.
33 +# * `crypto-openssl` links libssl/libcrypto, which both images
34 +# already name outright — libssl-dev in the builder for gix,
35 +# openssl in the runner for the Erlang runtime. It costs one
36 +# build-stage package, libclang-dev, because openssl-sys generates
37 +# its bindings with bindgen; every C-binding backend needs that,
38 +# so it is not a reason to prefer another. Constant-time, audited.
34 39 #
35 40 # So: openssl. `default-features = false` is what actually keeps nettle
36 41 # out; naming a backend alone does not.

Parents: cac71de