feat: add telegraf:<version>-distroless variant#892
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
| # Verified binary + default config + empty drop-in dir; users mount their own over these. | ||
| COPY --from=fetch /out/usr/bin/telegraf /usr/bin/telegraf | ||
| COPY --from=fetch /out/etc/telegraf/telegraf.conf /etc/telegraf/telegraf.conf | ||
| COPY --from=fetch /out/etc/telegraf/telegraf.d /etc/telegraf/telegraf.d |
There was a problem hiding this comment.
Why not
| # Verified binary + default config + empty drop-in dir; users mount their own over these. | |
| COPY --from=fetch /out/usr/bin/telegraf /usr/bin/telegraf | |
| COPY --from=fetch /out/etc/telegraf/telegraf.conf /etc/telegraf/telegraf.conf | |
| COPY --from=fetch /out/etc/telegraf/telegraf.d /etc/telegraf/telegraf.d | |
| # Inject telegraf | |
| ADD --checksum=${TELEGRAF_CHECKSUM} --unpack=true https://dl.influxdata.com/telegraf/releases/telegraf-${TELEGRAF_VERSION}_${ARCH}.tar.gz / |
There was a problem hiding this comment.
Updated the code, I tried this method but ran into some issues, I couldn't find a way to only unpack the binaries and the files that I needed.
This process unpacks the whole .tar in the root directory, which also has the binary with the version on it.
My approach in the current PR is a bit more generic by only copying the binary and naming it as telegraf, that way we don't need to change the entry point across Dockerfiles.
Let me know what you think or if you would like me to take this approach instead.
There was a problem hiding this comment.
Regarding the testing, I updated the circle-test.sh script in the root directory to also run the circle-test.sh file on each distroless sub directory, now this is automated on the pipeline 🥳
As for testing with different plugins I added instructions on how I tested this locally and also added a script that automates the same steps if anyone wants to validate this locally for the plugins you mentioned.
The Script/TESTING.md files are duplicated across directories for simplicity as I assume when you have a new version it will be copy/pasted, Let me know if you would prefer I unify them somewhere else.
There was a problem hiding this comment.
Testing TL;DR - does TLS and timezones actually work in the distroless image?
The existing circle-test.sh smoke tests don't touch two things I wanted to be sure about: whether the image can make outbound TLS calls (i.e. does it actually have the system CA bundle) and whether named timezones work (does tzdata ship with it). I wrote a little runbook + script for each distroless variant to check both by hand. It's not wired into CI yet; that's a separate follow-up once I've talked it through with the maintainers.
What I added (same bytes in all three versions):
telegraf/{1.37,1.38,1.39}/distroless/TESTING.md, the step-by-step so anyone can reproduce ittelegraf/{1.37,1.38,1.39}/distroless/local-test.sh, a one-shot script. It figures out its own tag from the directory it lives in, so I can copy it around without editing anything.
The two checks:
| What I'm checking | How | What "pass" looks like |
|---|---|---|
| Can it make a real HTTPS call and validate the cert? | inputs.http_response hitting https://dl.influxdata.com with insecure_skip_verify=false |
metric comes back withresult_code=0i |
| Does tzdata exist / do named zones resolve? | inputs.file + json_timezone, comparing UTC to America/Los_Angeles |
timestamps differ by exactly 28800000000000 ns (that's 8 hours) |
Results (ran these against images I built locally):
- ✅
1.37(Telegraf 1.37.3) and1.39(1.39.0), both checks pass with a clean exit 0. The same script correctly picked the right tag per version and built each one's Dockerfile. - ✅ Sanity check that it's not lying to me: I pointed the TLS check at
https://self-signed.badssl.comand gotresult_code=3i(connection_failed). That means the cert validation
genuinely fails on an untrusted cert, so the passing case actually means something. - ✅
shellcheckis clean, both the plain-docker andCONTAINER_CLI=podmanpaths work, and re-running is safe (it reuses the existing image instead of rebuilding).
One gotcha with podman: the distroless Dockerfile uses ADD --checksum --unpack, which is a BuildKit-only feature, and plain podman build/buildah just can't do it.
The script notices when you're on Podman and builds through a moby/buildkit container instead (the same trick docker buildx uses under the hood), then loads the image in. For what it's worth, I ran all of this with podman + moby because that's my setup, but the docker path should work just as well.
53f8cf0 to
d81672b
Compare
| circle_tests=$(find "$dir" -mindepth 2 -name circle-test.sh -print0) | ||
| for path in $circle_tests; do | ||
| log_msg "Executing $path" | ||
| . $path | ||
| done |
There was a problem hiding this comment.
I am not very familiar with CircleCI but this should allow your CI/CD to auto run the smoke tests I added on each /distroless directory (circle-test.sh), it was already built in but commented out 9 years ago on this commit
I can revert this if this could break your CI pipeline just let me know.
There was a problem hiding this comment.
ah! it was commented because it was actually borked.
It should be fixed now.
CircleCI output can be found here
e488b34 to
dece662
Compare
dece662 to
d587fc1
Compare

Add a
distrolessTelegraf variant (1.37, 1.38, 1.39)This adds an opt-in
distrolessvariant for each supported Telegraf version, producingtelegraf:{1.37,1.38,1.39}-distroless. Each 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/{ver}/distroless/Dockerfilefetch→scratch). The heart of the PR.telegraf/{ver}/distroless/circle-test.shtelegraf/{ver}/distroless/TESTING.md+local-test.shtelegraf/manifest.jsondistrolessfor1.37/1.38/1.39onamd64+arm64v8.circle-test.sh(root)circle-test.sh" loop (commented out before this branch). The onlycircle-test.shfiles at depth ≥2 in the repo are the three added by this PR, so the loop runs exactly the new distroless smoke tests; it does not execute or alter any existing image's tests.Each tag maps to a Telegraf release:
1.37-distroless1.38-distroless1.39-distrolessHow the Dockerfile works
It's a two-stage build:
fetchstage downloads the release, GPG-verifies it against InfluxData's signing key (24C975CBA61A024EE1B631787C3D57159FC2F927, the same key thealpinesibling uses), 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(noADD --unpack, noCOPY --parents, no other BuildKit-only feature), so it builds natively on the classic builder, docker/BuildKit, andpodman build/buildah./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/groupnonroot: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; the specific unsupported plugins/ports are documented only here.Why
scratch, and why a tag not a digestMost of the CVEs we hit come from the parent image's userland (distro packages, the shell, coreutils) rather than from Telegraf itself. See this Debian CVE, which had no fix on any stable release as of June 26, 2026, and which forces us to keep swapping between the Debian and Alpine images to stay ahead of scanners. Building
FROM 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 KubernetesrunAsNonRootand a read-only root filesystem out of the box.Two integrity/versioning choices changed 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 us a complete inventory of the image (see the table above) with nothing we don't use, and one fewer external base to track.TELEGRAF_VERSIONbuild arg and GPG-verified; the alpine fetch base is thealpine:3.23tag. Neither is pinned to asha256digest. This matches how InfluxData publishes to Docker Hub: a new Telegraf release is picked up by bumping the version and rebuilding, with no per-release digest to hand-edit. (The earlier revisionsha256-pinned both the tarball viaADD --checksum=and agcr.io/distroless/staticbase, each of which would need a manual digest bump on 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. They're sourced from the
alpine:3.23fetch base at build time. Because that's 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; that's the maintenance cost ofscratchversus consuming a maintained base. Note thatalpine:3.23is minor-pinned: once 3.23 reaches EOL a plain rebuild no longer refreshes the data and the fetch base must be bumped to the next series, an explicit part of that accepted cost. One caveat for scanning: 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
I built and ran all three locally on
linux/arm64with Podman (they also build with Docker), in both cases with a plain build (no BuildKit container, no--privileged). The fetch-stage GPG check described above gates every build.Reproduce locally (from any version's directory):
To build/inspect the image on its own:
docker build -t telegraf:1.39-distroless .(plainpodman buildalso works). The re-enabled rootcircle-test.shloop runs the smoke tests in CI; the full runbook is in eachtelegraf/{ver}/distroless/TESTING.md.Smoke tests (now run in CI via the re-enabled root loop), in
telegraf/{ver}/distroless/circle-test.sh:--versionreports the expected version65532/bin/shentrypoint override fails)telegraf --testexits 0 on the default configManual runtime checks: the two capabilities the smoke tests deliberately don't cover (
TESTING.md+local-test.sh)The smoke tests are hermetic and by design don't exercise two things the shell-less image relies on, so each version ships a manual runbook (
TESTING.md) and a one-shot script (local-test.sh, which derives its tag from its own directory so it stays byte-identical across versions). These are not wired into CI; automating them is a separate follow-up pending discussion with the maintainers.inputs.http_responseagainsthttps://dl.influxdata.com(insecure_skip_verify=false)result_code=0iinputs.file+json_timezone, UTC vsAmerica/Los_Angeles28800000000000ns (8h)Results from
local-test.sh(verified on all three versions):1.37(1.37.3),1.38(1.38.4), and1.39(1.39.0): both checks pass, clean exit 0, on the Podman path I ran. The same script picked the right tag per version and built each one's Dockerfile natively.TESTING.md's Troubleshooting section documents the negative case: pointing the TLS check athttps://self-signed.badssl.comyieldsresult_code=3i(connection_failed), confirming the CA bundle genuinely rejects an untrusted cert, so a passingresult_code=0irun actually means something. (local-test.shitself only runs the positive check.)shellcheckis clean, and re-runs are idempotent (local-test.shreuses the existing image unlessREBUILD=1).