feat(telegraf): Add distroless container based on scratch base-image#892
feat(telegraf): Add distroless container based on scratch base-image#892victor-gama wants to merge 1 commit into
Conversation
srebhan
left a comment
There was a problem hiding this comment.
Thanks a lot for your work @victor-gama!
I don't think we need the staging approach as we can utilize the docker commands to get, check and unpack the released Telegraf binary.
Furthermore, I would really love to learn on how the circle-test.sh approach works. Do we need to execute this manually or is this something auto-executed?
Please also test at least the following:
- use a plugin to connect to an https endpoint for verifying TLS works
- use a plugin to parse times with timezone information to make sure the timezone-info is available
53f8cf0 to
d81672b
Compare
dece662 to
d587fc1
Compare
srebhan
left a comment
There was a problem hiding this comment.
Thanks for the update @victor-gama! The PR looks mostly good, I have some small comments/questions...
Furthermore, can you please remove everything except v1.39!? I'm not planning to add this for older versions...
Another point are the testing scripts. Can we please remove them as we don't have those anywhere else and I don't want to add to credit consumption here...
| # GPG-verifies the release and assembles the exact final image tree under | ||
| # /rootfs; the scratch stage is a single COPY of it. | ||
|
|
||
| ARG TELEGRAF_VERSION=1.39.0 |
There was a problem hiding this comment.
Can we please have
| ARG TELEGRAF_VERSION=1.39.0 | |
| ENV TELEGRAF_VERSION=1.39.1 |
as this is the current version and we require ENV for our release toolchain... :-)
d587fc1 to
caf14d7
Compare
| # /rootfs; the scratch stage is a single COPY of it. | ||
|
|
||
| FROM alpine:3.23 AS fetch | ||
| ENV TELEGRAF_VERSION 1.39.1 |
There was a problem hiding this comment.
I had to duplicate this twice since ENV doesn't get inherited by the scratch image below, the alternative is having this defined as ARG and ENV, which ends up with the same scenario of the same string in two places.
Opted for just duplicating ENV twice to keep things consistent but is intentional, let me know if there is a better way to achieve this without the duplication and without using a global ARG.
There was a problem hiding this comment.
Maybe we should revert to the ARG version you had before. Thinking more about it, ENV is actually not what we want as the version is only relevant during build-time. What do you think?
I can fix the release toolchain then and you would need to fix all other Telegraf images... ;-)
Add a
distrolessTelegraf variant (1.39)This adds an opt-in
distrolessvariant for Telegraf 1.39, producingtelegraf:1.39-distroless. The image is builtFROM scratchand ships only the statictelegrafbinary plus the handful of files it needs at runtime (no shell, no libc, no package manager, no OS userland), and runs non-root (uid65532). The change is purely additive: the existingdefaultandalpineimages are untouched.What to review
telegraf/1.39/distroless/Dockerfilefetch→scratch). The heart of the PR.telegraf/manifest.jsondistrolessfor1.39onamd64+arm64v8.1.39-distrolessmaps to Telegraf release 1.39.1 (ENV TELEGRAF_VERSION 1.39.1).How the Dockerfile works
It's a two-stage build:
fetchstage downloads the release withwget, GPG-verifies the tarball against InfluxData's signing key (24C975CBA61A024EE1B631787C3D57159FC2F927— the same key thedefaultandalpinesiblings use), unpacks it, and assembles/rootfsas the exact final image tree (binary + config + CA bundle + tzdata +nsswitch.conf+passwd/group+home/tmp).scratchstage brings that tree over with a single plainCOPY --from=fetch /rootfs/ /— noADD --unpack, noCOPY --parents, no other BuildKit-only feature — so it isn't tied to any one builder.gpg --batch --verifycheck in the fetch stage; the build runs underset -euxand fails loudly if verification fails. Nothing from alpine reaches the final image except the files explicitly staged under/rootfs.scratch, so there are no OS packages to track or bump.What the final image contains: the complete inventory (everything a distro base would add is deliberately omitted)
/usr/bin/telegrafCGO_ENABLED=0) binary/etc/telegraf/{telegraf.conf,telegraf.d}/etc/ssl/certs/ca-certificates.crtinfluxdb_v2)/usr/share/zoneinfotime.LoadLocation,json_timezone, cron schedules/etc/nsswitch.confhosts: files dnsso Go's resolver checks/etc/hostsbefore DNS/etc/passwd,/etc/grouproot,nobody(65534),nonroot(65532)/home/nonroot(owned 65532)$HOMEfor uid 65532/tmp(owned 65532)os.TempDir()default;scratchships neitherNot a drop-in replacement
This is a hardening-focused variant, not a swap-in for the
default/alpineimages. Because there is no shell and nosetcap/setpriv, it does not support the ICMPpinginput, privileged ports (below 1024), or shell-out plugins likeexec,snmp, andsensors. It targets network-listener and push workloads. The Dockerfile header notes the image ships no shell or OS userland — the root cause of these limitations.Why
scratch, and why a tag not a digestMost CVEs flagged against the
default/alpineimages come from the parent image's userland (distro packages, the shell, coreutils) rather than from Telegraf itself (e.g. CVE-2026-48962), which is why we periodically swap between the Debian- and Alpine-based images to stay ahead of scanners. BuildingFROM scratchdrops that whole layer, so CVE exposure comes down to the Telegraf binary plus a maintained CA bundle and tzdata. The image also runs non-root, so it satisfies KubernetesrunAsNonRootout of the box.Two integrity/versioning choices in this revision, per @srebhan's feedback:
FROM scratch, not a distroless base image. Rather than depend ongcr.io/distroless/static, we assemble the rootfs ourselves in the alpine stage. That gives a complete inventory of the image (see the table above) with nothing we don't use, and one fewer external base to track.TELEGRAF_VERSIONand GPG-verified; the fetch base is thealpine:3.23tag. Neither issha256-pinned. This matches how InfluxData publishes to Docker Hub — a new release is picked up by bumping the version and rebuilding, with no per-release digest to hand-edit. (An earlier revisionsha256-pinned both the tarball viaADD --checksum=and agcr.io/distroless/staticbase, each of which would need a manual digest bump every release.) Integrity is still enforced, now by the GPG signature instead of a committed hash.The tradeoff we accept: we own refreshing the CA bundle and tzdata, both sourced from the
alpine:3.23fetch base at build time. Because3.23is a patch-rolling tag, a plain rebuild picks up alpine's latest CA/tzdata patches within the 3.23 series — so a periodic rebuild keeps the data fresh. Once 3.23 reaches EOL, the fetch base must be bumped to the next series (an explicit part of that accepted cost). One scanning caveat: ascratchimage exposes no OS-package database, so scanners report zero OS packages — a cleaner report, but the copied CA bundle and tzdata aren't scanner-visible.Testing
Built and run locally on
linux/arm64with Docker (plain build, no--privileged). The fetch-stage GPG check gates every build.docker buildsucceeds and the fetch stage reportsGood signature from "InfluxData Package Signing Key"; the build aborts if verification fails.telegraf --version→Telegraf 1.39.1.USERis65532:65532, and aprocstatrun (pid_finder = "native",--test) emitsuser="nonroot"— i.e. uid 65532 resolves to thenonrootname from the image's own/etc/passwd.pgrepin the image —procstat's defaultpgrepfinder errors withcould not find pgrep binary, and the pure-Gonativefinder is required.runAsNonRoot: in a localkindcluster, a pod withsecurityContext.runAsNonRoot: truereferencing this image is admitted and runs (Completed), confirming the numericUSER 65532passes the kubelet's non-root check. (A namedUSERwould fail that check — hence numeric.)Reproduce locally: