Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/image-trust.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Image trust

on:
pull_request:
branches:
- master
- release/*
paths:
- docker/**
- test/container-trust/**
- .github/workflows/image-trust.yaml
- .github/workflows/reusable_build_docker.yaml
workflow_dispatch:

permissions:
contents: read

concurrency:
group: image-trust-${{ github.ref }}
cancel-in-progress: true

jobs:
build-and-test-images:
strategy:
fail-fast: false
matrix:
arch: [x86_64, aarch64]
uses: ./.github/workflows/reusable_build_docker.yaml
with:
arch: ${{ matrix.arch }}
# Exercise image construction using an already published package. The
# package CI separately builds and tests the components pinned by this PR.
# Master and release workflows test these images with their new packages.
version: '0.45.1'
tag: trust-${{ github.run_id }}
31 changes: 27 additions & 4 deletions .github/workflows/reusable_build_docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:

- name: Build falco image
run: |
docker build -f docker/falco/Dockerfile -t docker.io/falcosecurity/falco:${{ inputs.arch }}-${{ inputs.tag }} \
docker build --pull --no-cache -f docker/falco/Dockerfile -t docker.io/falcosecurity/falco:${{ inputs.arch }}-${{ inputs.tag }} \
--build-arg VERSION_BUCKET=bin${{ inputs.bucket_suffix }} \
--build-arg FALCO_VERSION=${{ inputs.version }} \
--build-arg FALCO_COMMIT_SHA=${{ github.sha }} \
Expand All @@ -53,7 +53,7 @@ jobs:

- name: Build falco-debian image
run: |
docker build -f docker/falco-debian/Dockerfile -t docker.io/falcosecurity/falco:${{ inputs.arch }}-${{ inputs.tag }}-debian \
docker build --pull --no-cache -f docker/falco-debian/Dockerfile -t docker.io/falcosecurity/falco:${{ inputs.arch }}-${{ inputs.tag }}-debian \
--build-arg VERSION_BUCKET=deb${{ inputs.bucket_suffix }} \
--build-arg FALCO_VERSION=${{ inputs.version }} \
--build-arg FALCO_COMMIT_SHA=${{ github.sha }} \
Expand All @@ -62,8 +62,9 @@ jobs:
docker save docker.io/falcosecurity/falco:${{ inputs.arch }}-${{ inputs.tag }}-debian --output /tmp/falco-${{ inputs.arch }}-debian.tar

- name: Build falco-driver-loader image
# Use the Debian image built above; it has not been published yet.
run: |
docker build -f docker/driver-loader/Dockerfile -t docker.io/falcosecurity/falco-driver-loader:${{ inputs.arch }}-${{ inputs.tag }} \
docker build --no-cache -f docker/driver-loader/Dockerfile -t docker.io/falcosecurity/falco-driver-loader:${{ inputs.arch }}-${{ inputs.tag }} \
--build-arg FALCO_IMAGE_TAG=${{ inputs.arch }}-${{ inputs.tag }} \
--build-arg FALCO_VERSION=${{ inputs.version }} \
--build-arg FALCO_COMMIT_SHA=${{ github.sha }} \
Expand All @@ -73,14 +74,36 @@ jobs:

- name: Build falco-driver-loader-buster image
run: |
docker build -f docker/driver-loader-buster/Dockerfile -t docker.io/falcosecurity/falco-driver-loader:${{ inputs.arch }}-${{ inputs.tag }}-buster \
docker build --pull --no-cache -f docker/driver-loader-buster/Dockerfile -t docker.io/falcosecurity/falco-driver-loader:${{ inputs.arch }}-${{ inputs.tag }}-buster \
--build-arg VERSION_BUCKET=deb${{ inputs.bucket_suffix }} \
--build-arg FALCO_VERSION=${{ inputs.version }} \
--build-arg FALCO_COMMIT_SHA=${{ github.sha }} \
--build-arg TARGETARCH=${TARGETARCH} \
.
docker save docker.io/falcosecurity/falco-driver-loader:${{ inputs.arch }}-${{ inputs.tag }}-buster --output /tmp/falco-driver-loader-${{ inputs.arch }}-buster.tar

- name: Verify image certificate trust
env:
IMAGE_TAG: ${{ inputs.arch }}-${{ inputs.tag }}
run: |
for image in \
"docker.io/falcosecurity/falco:${IMAGE_TAG}" \
"docker.io/falcosecurity/falco:${IMAGE_TAG}-debian" \
"docker.io/falcosecurity/falco-driver-loader:${IMAGE_TAG}" \
"docker.io/falcosecurity/falco-driver-loader:${IMAGE_TAG}-buster"; do
echo "Checking ${image}"
docker run --rm --network none --entrypoint /bin/sh \
--mount "type=bind,src=${GITHUB_WORKSPACE}/test/container-trust,dst=/test-trust,readonly" \
"${image}" -ec '
sh /test-trust/check-trust.sh
update-ca-certificates --fresh
sh /test-trust/check-trust.sh
'
docker run --rm --entrypoint curl "${image}" \
--fail --silent --show-error --location --max-time 60 \
--output /dev/null https://download.falco.org/
done

- name: Upload images tarballs
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
Expand Down
6 changes: 3 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,11 @@ if(NOT WIN32
AND NOT MUSL_OPTIMIZED_BUILD
)
include(falcoctl)
set(CONTAINER_VERSION "0.7.5")
set(CONTAINER_VERSION "0.7.6")
if(${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "x86_64")
set(CONTAINER_HASH "ac29cbb16aa3c9bbb290e6533aef9a9a39943ffa1be6e1a2ed8ff1c3780b5d9b")
set(CONTAINER_HASH "44d61eff009ed9b693ad065b07738d5b90cc264fe195d3d11a24e94e60e17bb1")
else() # arm64
set(CONTAINER_HASH "907a54f1bdbad59cbc137859c66be1f315c3556ecf42b012592fb6b5818b0374")
set(CONTAINER_HASH "3971b047c95d656eb4dd420385ba7a7035f0b3c3d874301dca66a241403c92e7")
endif()
include(container_plugin)

Expand Down
6 changes: 3 additions & 3 deletions cmake/modules/falcoctl.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ option(ADD_FALCOCTL_DEPENDENCY "Add falcoctl dependency while building falco" ON
if(ADD_FALCOCTL_DEPENDENCY)
string(TOLOWER ${CMAKE_HOST_SYSTEM_NAME} FALCOCTL_SYSTEM_NAME)

set(FALCOCTL_VERSION "0.14.2")
set(FALCOCTL_VERSION "0.14.3")

message(STATUS "Building with falcoctl: ${FALCOCTL_VERSION}")

if(${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "x86_64")
set(FALCOCTL_SYSTEM_PROC_GO "amd64")
set(FALCOCTL_HASH "9e0c12abdee6f042cffaf33b8bc4103e70f68a597f4d02310e2aa06a860310df")
set(FALCOCTL_HASH "263a6c773b4d24af1e6827ea2f63ac7c50678becf58e607390eb8976dee08af1")
else() # aarch64
set(FALCOCTL_SYSTEM_PROC_GO "arm64")
set(FALCOCTL_HASH "f8fda77552eb1cdf100d827de38b3f38401237325c541f4e020c94cf75f8cec8")
set(FALCOCTL_HASH "527a81cba5e42b283466aa74942d0eb6ea044aa969f9cf86f3ef856cbe636b36")
endif()

ExternalProject_Add(
Expand Down
14 changes: 11 additions & 3 deletions docker/driver-loader-buster/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,19 @@ RUN apt-get update \
patchelf \
xz-utils \
zstd \
&& if [ "$TARGETARCH" = "amd64" ]; then apt-get install -y --no-install-recommends libmpx2; fi \
&& rm -rf /var/lib/apt/lists/*

RUN if [ "$TARGETARCH" = "amd64" ]; \
then apt-get install -y --no-install-recommends libmpx2; \
fi
# The frozen Buster repository cannot supply the eMudhra trust-store fix.
# Disable the affected anchors, including the legacy AAA cross-signing root,
# before any HTTPS downloads. Keep the exclusions when regenerating the store.
RUN sed -i \
-e 's|^mozilla/emSign_ECC_Root_CA_-_G3.crt$|!&|' \
-e 's|^mozilla/emSign_Root_CA_-_C1.crt$|!&|' \
-e 's|^mozilla/emSign_ECC_Root_CA_-_C3.crt$|!&|' \
-e 's|^mozilla/Comodo_AAA_Services_root.crt$|!&|' \
/etc/ca-certificates.conf \
&& update-ca-certificates --fresh

# gcc 6 is no longer included in debian stable, but we need it to
# build kernel modules on the default debian-based ami used by
Expand Down
2 changes: 2 additions & 0 deletions docker/driver-loader/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ RUN apt-get update \
patchelf \
xz-utils \
zstd \
&& dpkg --compare-versions "$(dpkg-query -W -f='${Version}' ca-certificates)" ge '20250419~deb12u2' \
&& update-ca-certificates --fresh \
&& rm -rf /var/lib/apt/lists/*

# Some base images have an empty /lib/modules by default
Expand Down
2 changes: 2 additions & 0 deletions docker/falco-debian/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ ENV FALCO_VERSION="${FALCO_VERSION}" \
HOME="${HOME}"

RUN apt-get -y update && apt-get -y install curl jq ca-certificates gnupg \
&& dpkg --compare-versions "$(dpkg-query -W -f='${Version}' ca-certificates)" ge '20250419~deb12u2' \
&& update-ca-certificates --fresh \
&& apt clean -y && rm -rf /var/lib/apt/lists/*

WORKDIR /
Expand Down
4 changes: 3 additions & 1 deletion docker/falco/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ ENV FALCO_VERSION="${FALCO_VERSION}" \
HOST_ROOT="${HOST_ROOT}" \
HOME="${HOME}"

RUN apk update && apk add curl ca-certificates jq libstdc++
RUN apk add --no-cache --upgrade curl jq libstdc++ openssl \
'ca-certificates>=20260909-r3' 'ca-certificates-bundle>=20260909-r3' \
&& update-ca-certificates

WORKDIR /

Expand Down
45 changes: 45 additions & 0 deletions test/container-trust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Container CA trust regression test

[check-trust.sh](check-trust.sh) checks the image's OpenSSL trust bundle and hashed CA directory independently. It makes no network requests and changes no trust files. It requires a POSIX shell, OpenSSL, `dirname`, and `grep`.

Run it inside the image, with this directory mounted read-only:

```sh
docker run --rm --network none --entrypoint /bin/sh \
-v "$PWD/test/container-trust:/trust:ro" IMAGE \
-c '/trust/check-trust.sh && update-ca-certificates --fresh && /trust/check-trust.sh'
```

The second run detects trust restored by certificate-store regeneration. Use a disposable container for regeneration. The script defaults to `/etc/ssl/certs/ca-certificates.crt` and `/etc/ssl/certs`; optional positional arguments select a different bundle and hashed directory. `OPENSSL` can select an executable. These overrides also allow host-side inspection of extracted stores, but that does not test the image's own OpenSSL or regeneration command.

## Checks

- All 14 [eMudhra advisory](https://emudhra.we-have-a-problem.com/) server chains must fail with OpenSSL trust error 20 in **both** store modes. Certificate expiry, hostname mismatch, malformed fixtures and unsupported command options are failures of the test, not evidence of distrust.
- Every advisory chain must first **pass** against the explicit historical trust roots in [legacy-roots.pem](fixtures/controls/legacy-roots.pem). This confirms that each fixture still detects the original exposure.
- A legitimate [download.falco.org chain](fixtures/controls/download.falco.org.pem) must pass server-purpose and hostname verification through each tested store. It must fail against the unrelated legacy roots, checking that implicit system trust cannot rescue an explicit store.
- The unaffected [ISRG Root X1](fixtures/controls/isrg-root-x1.pem) and [DigiCert Global Root G2](fixtures/controls/digicert-global-root-g2.pem) must remain trusted in both stores. Roots use purpose `any`, because a CA's key usage need not allow it to act as a server leaf.

All verifications use **2026-10-23T12:00:00Z** (`1792756800`), when the saved server chains are valid. The short-lived advisory leaves would otherwise stop detecting regressions after expiry. Do not change this time without revalidating every fixture and control.

The script disables default CA files and directories. It also disables the default CA store on OpenSSL versions that provide that option; OpenSSL 1.1.1 has no separate CA-store option. File checks cannot fall back to the image's directory, and directory checks cannot fall back to its bundle. Positive directory checks require working hash entries, not just PEM files in a directory.

This checks OpenSSL chain validation and trust-store contents. It does not test revocation, Certificate Transparency enforcement, Go's verifier, or a network TLS handshake. A separate live HTTPS smoke test can check the current-time network path.

## Fixture provenance

Only public certificates are included; there are no private keys. **Never install the legacy roots into an image's trust store.** They are deliberately trusted only in isolated test invocations.

| Files | Source |
|---|---|
| [rejected/](fixtures/rejected/) — 14 complete server chains, leaf first | The advisory's published offline chain archive, retrieved 2026-10-23. Its README dates the captures to October 20 and the two `noct` chains to October 22. The non-`aaa` chains use the emSign ECC G3 trust path; the `aaa` variants use AAA Certificate Services. The `noct` variants omit SCTs. |
| [legacy-roots.pem](fixtures/controls/legacy-roots.pem) | AAA Certificate Services and emSign ECC Root CA - G3 public roots, extracted from [certifi 2024.12.14](https://github.com/certifi/python-certifi/blob/2024.12.14/certifi/cacert.pem), which carries the historical Mozilla root collection. |
| [isrg-root-x1.pem](fixtures/controls/isrg-root-x1.pem), [digicert-global-root-g2.pem](fixtures/controls/digicert-global-root-g2.pem) | Unmodified public root certificates from the same historical certifi collection. |
| [download.falco.org.pem](fixtures/controls/download.falco.org.pem) | Public chain captured from `download.falco.org:443` with SNI on 2026-10-26: leaf → Let's Encrypt YR2 → cross-signed ISRG Root YR → trusted ISRG Root X1. The fixture contains the three server-supplied certificates, not the self-signed root. The leaf is valid September 25–December 24, 2026. |

[SHA256SUMS](fixtures/SHA256SUMS) records the curated PEM file hashes. To verify them from this directory:

```sh
(cd fixtures && sha256sum -c SHA256SUMS)
```

When adding or replacing fixtures, verify the positive result against their explicit historical anchors, the rejection against fixed stores, and the independent positive server chain at the fixed time. Keep the expected advisory count synchronized only when intentionally changing coverage.
122 changes: 122 additions & 0 deletions test/container-trust/check-trust.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2026 The Falco Authors.
# Offline regression test for the eMudhra CA distrust, using the image's OpenSSL.
set -eu
export LC_ALL=C

if [ "$#" -gt 2 ]; then
echo "Usage: $0 [CA_BUNDLE [CA_PATH]]" >&2
exit 2
fi

bundle=${1:-/etc/ssl/certs/ca-certificates.crt}
capath=${2:-/etc/ssl/certs}
openssl=${OPENSSL:-openssl}
fixtures=$(CDPATH= cd "$(dirname "$0")/fixtures" && pwd)
# The advisory leaves are deliberately short-lived. Never use the current time.
verify_time=1792756800 # 2026-10-23T12:00:00Z

fail() {
printf 'FAIL: %s\n' "$*" >&2
exit 1
}

command -v "$openssl" >/dev/null 2>&1 || fail "openssl is required"
[ -s "$bundle" ] || fail "missing or empty CA bundle: $bundle"
[ -d "$capath" ] || fail "missing CA directory: $capath"
set -- "$fixtures"/rejected/*.pem
[ "$#" -eq 14 ] || fail "expected exactly 14 advisory chain fixtures"

# OpenSSL 1.1.1 has no CAstore option; OpenSSL 3 adds another default store.
# Disable every implicit source, even when an explicit CAfile/CApath is supplied.
verify_help=$("$openssl" verify -help 2>&1 || true)
has_castore=false
case "$verify_help" in *-no-CAstore*) has_castore=true ;; esac

verify() {
store_mode=$1
store=$2
certificate=$3
hostname=$4
set -- -no-CAfile -no-CApath -attime "$verify_time"
if [ "$has_castore" = true ]; then
set -- "$@" -no-CAstore
fi
case "$store_mode" in
bundle) set -- "$@" -CAfile "$store" ;;
capath) set -- "$@" -CApath "$store" ;;
*) fail "unknown trust-store mode: $store_mode" ;;
esac
if [ -n "$hostname" ]; then
set -- "$@" -purpose sslserver -verify_hostname "$hostname"
else
# A root CA is not itself a server leaf (its key usage may only allow
# signing certificates). Check its presence in the store without asking
# it to authenticate as a TLS server.
set -- "$@" -purpose any
fi
"$openssl" verify "$@" -untrusted "$certificate" "$certificate"
}

expect() {
expected=$1
label=$2
shift 2
status=0
result=$(verify "$@" 2>&1) || status=$?
case "$expected" in
accept)
if [ "$status" -ne 0 ]; then
printf '%s\n' "$result" >&2
fail "$label must verify"
fi
;;
reject)
[ "$status" -ne 0 ] || fail "$label is unexpectedly trusted"
# Require missing trust, not expiry, hostname mismatch, parse errors,
# an unsupported option, or a failed executable. Fixture controls
# below independently prove each chain is valid at the fixed time.
if ! printf '%s\n' "$result" | grep -Eq 'error 20 at [0-9]+ depth lookup'; then
printf '%s\n' "$result" >&2
fail "$label failed for a reason other than missing trust"
fi
if printf '%s\n' "$result" | grep -E 'error [0-9]+ at' | grep -Evq 'error 20 at'; then
printf '%s\n' "$result" >&2
fail "$label also reported a non-trust verification error"
fi
;;
*) fail "unknown expected result: $expected" ;;
esac
printf 'PASS: %s (%s)\n' "$label" "$expected"
}

printf 'Checking trust at 2026-10-23T12:00:00Z with %s\n' "$("$openssl" version)"

# Explicit legacy trust proves that all 14 fixtures would expose the regression.
# These roots are test inputs only; they must never be installed in the image.
legacy_roots=$fixtures/controls/legacy-roots.pem
for chain in "$fixtures"/rejected/*.pem; do
host=${chain##*/}
host=${host%.pem}
expect accept "legacy control: $host" bundle "$legacy_roots" "$chain" "$host"
done

positive=$fixtures/controls/download.falco.org.pem
# An unrelated explicit store must not silently fall back to system trust.
expect reject 'isolated-store control: download.falco.org' bundle "$legacy_roots" "$positive" download.falco.org

for mode in bundle capath; do
case "$mode" in bundle) store=$bundle ;; capath) store=$capath ;; esac
expect accept "$mode: download.falco.org" "$mode" "$store" "$positive" download.falco.org
for root in isrg-root-x1 digicert-global-root-g2; do
expect accept "$mode: unaffected $root" "$mode" "$store" "$fixtures/controls/$root.pem" ''
done
for chain in "$fixtures"/rejected/*.pem; do
host=${chain##*/}
host=${host%.pem}
expect reject "$mode: $host" "$mode" "$store" "$chain" "$host"
done
done

echo 'PASS: both trust stores reject all 14 compromised chains and preserve the positive controls'
18 changes: 18 additions & 0 deletions test/container-trust/fixtures/SHA256SUMS
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
5d550643b6400d4341550a9b14aedd0b4fac33ae5deb7d8247b6b4f799c13306 controls/digicert-global-root-g2.pem
29e79f220ca013f22e95db1ea488742607f7e683efa7b889e98f838463cb63ba controls/download.falco.org.pem
22b557a27055b33606b6559f37703928d3e4ad79f110b407d04986e1843543d1 controls/isrg-root-x1.pem
3cbcc3e794449430c7d15a4d8f26eb3341698a01717b392d99ba702793afb74b controls/legacy-roots.pem
3641ff8e872e4c90d5691edb09894914121b267e6a50455347c5f28708fa1af2 rejected/dv-g3b-aaa-emudhra.we-have-a-problem.com.pem
a360266958b7dfc04250553f6a5f55be769457f126bfd4de415bbaffc9cf62ad rejected/dv-g3b-aaa-noct-emudhra.we-have-a-problem.com.pem
ab6483de0cb01d53d59275baf74d2766f3599e57a48d3319e4a5536c268ac696 rejected/dv-g3b-emudhra.we-have-a-problem.com.pem
de597b4e346d6e84d264a6bc62f5cb5bf4a4d64ec5e587edc446016e4aa693c5 rejected/dv-g3b-noct-emudhra.we-have-a-problem.com.pem
23641e6de1b0c5a452bd0aaaaa1647863cf5daf27e0b62611ea9289dc0106d39 rejected/dv-g3c-aaa-emudhra.we-have-a-problem.com.pem
370e97ac882a900d25bda5b8ffe0136197e41d1ab424c3f04adad77825283803 rejected/dv-g3c-emudhra.we-have-a-problem.com.pem
19fcef2a9fa3ecc88294736d1bdcc2cd7c8753960ff67e8890affa13bd5ea4e5 rejected/ev-g3b-aaa-emudhra.we-have-a-problem.com.pem
000c2f6b03a9903488ee18e09c6b86e22c2476d0052e11888e5f924de6ad0d44 rejected/ev-g3b-emudhra.we-have-a-problem.com.pem
6a3019bee019f96570bffca9002637655e7fa8221a370021ca6e0b0fd66f576d rejected/ev-g3c-aaa-emudhra.we-have-a-problem.com.pem
636f60b161812118b0474d5fdc1e194aaa1913ebc06331e0f24b324f08ba9a73 rejected/ev-g3c-emudhra.we-have-a-problem.com.pem
402cc78d8effe1157b92fbe23ffdb860a6ac754bd126692f98d4b1d2df66e35d rejected/ov-g3b-aaa-emudhra.we-have-a-problem.com.pem
c80265d7a38b7378544488d35c317598d5d8b9a1444468f68aa96b134d561d0b rejected/ov-g3b-emudhra.we-have-a-problem.com.pem
4cf2e52ff28f08d690046a75def341abf6508d18c197c6825c7820e0899e1c63 rejected/ov-g3c-aaa-emudhra.we-have-a-problem.com.pem
45e16413868da62601227eb792c9d8a2172f0765fde3e7f21524e188bd3a1f41 rejected/ov-g3c-emudhra.we-have-a-problem.com.pem
Loading
Loading