Skip to content

feat(telegraf): Add distroless container based on scratch base-image#892

Open
victor-gama wants to merge 1 commit into
influxdata:masterfrom
victor-gama:victor-gama/addTelegrafDistroless
Open

feat(telegraf): Add distroless container based on scratch base-image#892
victor-gama wants to merge 1 commit into
influxdata:masterfrom
victor-gama:victor-gama/addTelegrafDistroless

Conversation

@victor-gama

@victor-gama victor-gama commented Jun 26, 2026

Copy link
Copy Markdown

Add a distroless Telegraf variant (1.39)

This adds an opt-in distroless variant for Telegraf 1.39, producing telegraf:1.39-distroless. The image is built FROM scratch and ships only the static telegraf binary plus the handful of files it needs at runtime (no shell, no libc, no package manager, no OS userland), and runs non-root (uid 65532). The change is purely additive: the existing default and alpine images are untouched.

What to review

File What changed
telegraf/1.39/distroless/Dockerfile New. Two-stage build (alpine fetchscratch). The heart of the PR.
telegraf/manifest.json Registers distroless for 1.39 on amd64 + arm64v8.

1.39-distroless maps to Telegraf release 1.39.1 (ENV TELEGRAF_VERSION 1.39.1).

Scope note: this revision covers 1.39 only, and ships no test scripts — both per @srebhan's feedback (drop the extra versions; remove the TESTING.md/local-test.sh/circle-test.sh additions to avoid CI credit consumption). The version pin was also moved from ARG to ENV, as requested. If 1.37/1.38 variants are wanted, they're a trivial follow-up (same Dockerfile, different version).

How the Dockerfile works

It's a two-stage build:

  • An alpine fetch stage downloads the release with wget, GPG-verifies the tarball against InfluxData's signing key (24C975CBA61A024EE1B631787C3D57159FC2F927 — the same key the default and alpine siblings use), unpacks it, and assembles /rootfs as the exact final image tree (binary + config + CA bundle + tzdata + nsswitch.conf + passwd/group + home/tmp).
  • The final scratch stage brings that tree over with a single plain COPY --from=fetch /rootfs/ / — no ADD --unpack, no COPY --parents, no other BuildKit-only feature — so it isn't tied to any one builder.
  • Integrity is the gpg --batch --verify check in the fetch stage; the build runs under set -eux and fails loudly if verification fails. Nothing from alpine reaches the final image except the files explicitly staged under /rootfs.
  • The runtime base is 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)
Path Why it's needed
/usr/bin/telegraf the GPG-verified static (CGO_ENABLED=0) binary
/etc/telegraf/{telegraf.conf,telegraf.d} default config + drop-in dir
/etc/ssl/certs/ca-certificates.crt TLS trust roots for outbound HTTPS (e.g. influxdb_v2)
/usr/share/zoneinfo IANA tz database: time.LoadLocation, json_timezone, cron schedules
/etc/nsswitch.conf hosts: files dns so Go's resolver checks /etc/hosts before DNS
/etc/passwd, /etc/group deterministic identities: root, nobody (65534), nonroot (65532)
/home/nonroot (owned 65532) $HOME for uid 65532
/tmp (owned 65532) os.TempDir() default; scratch ships neither

Not a drop-in replacement

This is a hardening-focused variant, not a swap-in for the default/alpine images. Because there is no shell and no setcap/setpriv, it does not support the ICMP ping input, privileged ports (below 1024), or shell-out plugins like exec, snmp, and sensors. 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 digest

Most CVEs flagged against the default/alpine images 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. Building FROM scratch drops 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 Kubernetes runAsNonRoot out of the box.

Two integrity/versioning choices in this revision, per @srebhan's feedback:

  • FROM scratch, not a distroless base image. Rather than depend on gcr.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.
  • Pull by tag/version, not by pinned digest. The release is selected by TELEGRAF_VERSION and GPG-verified; the fetch base is the alpine:3.23 tag. Neither is sha256-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 revision sha256-pinned both the tarball via ADD --checksum= and a gcr.io/distroless/static base, 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.23 fetch base at build time. Because 3.23 is 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: a scratch image 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/arm64 with Docker (plain build, no --privileged). The fetch-stage GPG check gates every build.

  • Build + integrity: docker build succeeds and the fetch stage reports Good signature from "InfluxData Package Signing Key"; the build aborts if verification fails.
  • Version: telegraf --versionTelegraf 1.39.1.
  • Non-root identity: the image's USER is 65532:65532, and a procstat run (pid_finder = "native", --test) emits user="nonroot" — i.e. uid 65532 resolves to the nonroot name from the image's own /etc/passwd.
  • Genuinely shell-less: no shell or pgrep in the image — procstat's default pgrep finder errors with could not find pgrep binary, and the pure-Go native finder is required.
  • Kubernetes runAsNonRoot: in a local kind cluster, a pod with securityContext.runAsNonRoot: true referencing this image is admitted and runs (Completed), confirming the numeric USER 65532 passes the kubelet's non-root check. (A named USER would fail that check — hence numeric.)

Reproduce locally:

cd telegraf/1.39/distroless
docker build -t telegraf:1.39-distroless .   # plain `podman build` works too

docker run --rm telegraf:1.39-distroless --version           # -> Telegraf 1.39.1
docker inspect -f '{{.Config.User}}' telegraf:1.39-distroless # -> 65532:65532

@srebhan srebhan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread telegraf/1.39/distroless/Dockerfile Outdated
@victor-gama
victor-gama force-pushed the victor-gama/addTelegrafDistroless branch from 53f8cf0 to d81672b Compare July 5, 2026 03:30
Comment thread circle-test.sh Outdated
@victor-gama
victor-gama force-pushed the victor-gama/addTelegrafDistroless branch 4 times, most recently from dece662 to d587fc1 Compare July 9, 2026 21:41
@victor-gama
victor-gama requested a review from srebhan July 13, 2026 16:03
@srebhan srebhan changed the title feat: add telegraf:<version>-distroless variant feat(telegraf): Add container based on the scratch base-image Jul 14, 2026

@srebhan srebhan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

Comment thread telegraf/1.39/distroless/Dockerfile
Comment thread telegraf/1.39/distroless/Dockerfile
Comment thread telegraf/1.39/distroless/Dockerfile Outdated
# 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please have

Suggested change
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... :-)

@srebhan srebhan changed the title feat(telegraf): Add container based on the scratch base-image feat(telegraf): Add distroless container based on scratch base-image Jul 15, 2026
@victor-gama
victor-gama force-pushed the victor-gama/addTelegrafDistroless branch from d587fc1 to caf14d7 Compare July 17, 2026 03:08
# /rootfs; the scratch stage is a single COPY of it.

FROM alpine:3.23 AS fetch
ENV TELEGRAF_VERSION 1.39.1

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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... ;-)

Comment thread telegraf/1.39/distroless/Dockerfile
Comment thread telegraf/1.39/distroless/Dockerfile
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants