trustbom

command module
v0.0.0-...-857afb9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 22, 2026 License: MIT Imports: 12 Imported by: 0

README

trustbom

An SBOM for the part of your container nobody declared: its TLS trust store.

$ trustbom inspect alpine:latest
alpine:latest
  path      /etc/ssl/certs/ca-certificates.crt
  sha256    b8d837841b88bfaa1a0fa827cbca8e2576418dd47c9fc4bb7f1f9d89c83111b9
  roots     119

  shadowed, present but never read by Go:
    /etc/ssl/cert.pem

  by organization
      6    5.0%  D-Trust GmbH
      6    5.0%  SSL Corporation
      5    4.2%  DigiCert Inc
      4    3.4%  Amazon
      ...

  by country
     35   29.4%  US
     13   10.9%  DE
     12   10.1%  CN
      ...

The problem

Ask most teams which certificate authorities their service trusts and you get a blank look, because nobody decided. A typical Go Dockerfile ends like this:

FROM golang:1.26-alpine AS build
RUN apk add -U --no-cache ca-certificates
...
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

The service now trusts whatever Alpine trusted on the day that layer was cached. That set changes when the build image is bumped. Nothing announces it, nothing fails, and the diff shows one unchanged line.

Two ways it goes wrong, both seen in the wild:

  • The bundle silently changes. A Debian ca-certificates bump changed which roots were trusted and broke TLS for people using distroless images (distroless#545). DST Root CA X3 expiring broke Alpine-based images broadly (docker-alpine#206, still open).
  • The wrong file gets copied. On a Red Hat base, tls-ca-bundle.pem sits in a directory with ca-bundle.trust.crt, email-ca-bundle.pem and objsign-ca-bundle.pem. Copy the first by mistake and the image loads zero roots and fails every request with x509: certificate signed by unknown authority. Copy email-ca-bundle.pem and it is worse, because it does not look broken: measured, that swaps 75 TLS roots for 32 S/MIME ones, leaves 71 in common, and the common ones are enough that the obvious endpoints still work. Go does not honour trust bits, so nothing objects. A count does not catch it (146 becomes 103, both plausible) and a smoke test does not catch it. Only the set of fingerprints does.
  • A file gets added. Go reads the first file from its list and then appends every file in /etc/ssl/certs and /etc/pki/tls/certs. One extra .pem in either directory is a root the process trusts while the named bundle is byte for byte unchanged. trustbom models the whole load, so it reports that file by name.

A container's trust store is the one dependency with no manifest, no lock file and no review. trustbom gives it all three.

Install

go install github.com/cniackz/trustbom@latest

Or build from source: go build -o trustbom .

Use

Look at what an image trusts
trustbom inspect ghcr.io/me/api:v1.4.0
trustbom inspect registry.access.redhat.com/ubi9/ubi-minimal --platform linux/s390x
trustbom inspect alpine:latest --json | jq '.bundle.roots[].subject'

The image is never run. trustbom reads its layers straight from the registry, so this works on a FROM scratch image with no shell, on an image built for an architecture the checking machine cannot execute, and inside any CI job that can reach the registry.

Pin it, then hold builds to it
trustbom lock   ghcr.io/me/api:v1.4.0    # writes trustbom.lock
git add trustbom.lock                    # the trust store is now in review
trustbom verify ghcr.io/me/api:v1.5.0    # exit 2 if it changed

trustbom.lock pins the SHA-256 fingerprint of every root, not a count. A count is a smoke alarm: it moves when something is wrong, and it stays perfectly still when one authority is swapped for another, which is the change most worth seeing.

When a build differs you get the diff, not a number:

trust store differs from trustbom.lock

  the file Go reads moved: /etc/ssl/certs/ca-certificates.crt -> /etc/pki/tls/certs/ca-bundle.crt

  - CN=OISTE Server Root ECC G1,O=OISTE Foundation,C=CH
    eec997c0c30f216f7e3b8b307d2bae42412d753fc8219dafd1520b2572850f49
  + CN=AffirmTrust Commercial,O=AffirmTrust,C=US
    0376ab1d54c5f9803ce4b2e201a0ee7eef7b57b636e8a93c9b8d4860c96f5fa7

If this change is intended, re-run trustbom lock and commit the result.
Ask whether the browsers still trust those roots

inspect tells you which authorities are in the image. audit asks the Common CA Database, the register Mozilla, Chrome, Microsoft and Apple keep together, whether each one is still trusted for TLS:

$ trustbom audit alpine:latest
alpine:latest
  checked against CCADB, 172 roots included by Mozilla as of 2026-08-22

  117  trusted for websites
    1  listed, but not for websites
    1  not in Mozilla's included set

  listed without the websites trust bit. This is where a root lands
  after a TLS distrust, so a bundle still carrying one is trusting
  something the browsers stopped trusting for TLS:
    CN=Atos TrustedRoot 2011,O=Atos,C=DE
      trust bits: Email

The middle category is the interesting one. A root that loses a TLS distrust is not deleted from CCADB, it stays listed with its Websites trust bit removed, and in the bundle file it still looks like an ordinary CERTIFICATE block. Nothing short of this lookup can tell the difference.

--require-websites turns it into a gate that exits 2.

This is deliberately not part of lock or verify. A fingerprint is a fact about an image and has to mean the same thing offline and a year from now. Whether CCADB currently lists it is a fact about today, and it changes without the image changing. Folding the two together would make a build fail for a reason that has nothing to do with the build.

In CI
- name: The trust store is what we said it is
  run: |
    go install github.com/cniackz/trustbom@latest
    trustbom verify ${{ env.IMAGE }} --platform linux/amd64

Exit codes are meant to be acted on separately:

Code Meaning
0 as expected
1 usage error, or the registry could not be reached
2 the trust store differs from the lock, or the image has none

A registry outage should not read as a security finding, which is why 1 and 2 are not the same.

What it checks

  • Which file Go actually opens. crypto/x509 consults six paths in a fixed order and stops at the first that exists, so a file at the first path shadows a bundle at the fifth. trustbom reports the winner and names the shadowed ones, which is how an image ends up carrying a current bundle and using an older one.
  • Symlinks. Red Hat images link /etc/pki/tls/certs/ca-bundle.crt to the extracted bundle. A tool that reads the named path without following the link finds an empty file and reports a broken trust store that is fine.
  • Blocks a TLS client cannot use. BEGIN TRUSTED CERTIFICATE and malformed DER are counted, and a bundle with no usable roots is an error rather than a zero.
  • Expiry. Roots already past notAfter, and roots expiring within a year. An expired root is dead weight rather than a hole, but it is a reliable sign nobody has looked at this file in a long time.
  • Who you trust. Roots grouped by issuing organization and by country. Most images trust something over fifty organizations across thirty countries. Any one of them can mint a certificate your service will accept.
  • Whether the browsers still agree, via audit and CCADB: which roots are trusted for websites, which are listed for something else, and which are not listed at all.

What it does not do

It does not dial anything. Knowing an image carries the roots you expect is not the same as knowing TLS works, and the honest way to learn the second is to make a real connection from inside the image to the endpoints the service actually uses. docker-library/official-images has done exactly that for years to gate the openjdk and python official images. trustbom covers the half that can be answered without running anything; run the other half too.

It reads Go's search order. A Python, Java or Rust service resolves its trust store differently, and the file trustbom names is the one Go would open.

License

MIT.

Documentation

Overview

Command trustbom reports and pins the certificate authorities a container image asks its TLS clients to trust.

A container's trust store is usually not chosen. It arrives as a side effect of whichever base image the build stage happened to use, it changes when that image is rebuilt, and nothing fails when it does. The failure surfaces later, in production, as x509: certificate signed by unknown authority.

trustbom makes the trust store something a repository states and a build checks.

Directories

Path Synopsis
internal
bundle
Package bundle parses a PEM certificate bundle and describes what is in it.
Package bundle parses a PEM certificate bundle and describes what is in it.
ccadb
Package ccadb answers whether a root is one the browsers still trust for TLS.
Package ccadb answers whether a root is one the browsers still trust for TLS.
image
Package image pulls a container image and finds the CA bundle inside it, without running the image.
Package image pulls a container image and finds the CA bundle inside it, without running the image.
lock
Package lock records what an image is expected to trust, and reports how a later build differs from that record.
Package lock records what an image is expected to trust, and reports how a later build differs from that record.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL