Skip to content
Open
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
161 changes: 161 additions & 0 deletions .github/workflows/publish-dex-security-image.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
name: Publish Dex Security Image

on:
pull_request:
branches: [main]
paths:
- .github/workflows/publish-dex-security-image.yaml
- scripts/build-dex-security-image.sh
- scripts/check-dex-binary/**
- go.mod
- go.sum
- .github/scripts/setup-supply-chain-tools.sh
- k8s/bases/infrastructure/controllers/dex/DexSecurity.Dockerfile
- k8s/bases/infrastructure/controllers/dex/dex-v2.46.1-network.patch
- k8s/bases/infrastructure/controllers/dex/IMAGE-NOTICE
push:
branches: [main]
paths:
- .github/workflows/publish-dex-security-image.yaml
- scripts/build-dex-security-image.sh
- scripts/check-dex-binary/**
- go.mod
- go.sum
- .github/scripts/setup-supply-chain-tools.sh
- k8s/bases/infrastructure/controllers/dex/DexSecurity.Dockerfile
- k8s/bases/infrastructure/controllers/dex/dex-v2.46.1-network.patch
- k8s/bases/infrastructure/controllers/dex/IMAGE-NOTICE
Comment thread
devantler marked this conversation as resolved.
Resolved
workflow_dispatch:

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

env:
IMAGE: ghcr.io/devantler-tech/platform-dex
IMAGE_TAG: v2.46.1-security.1-${{ github.sha }}

jobs:
verify:
name: Verify native Dex distribution
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
outputs:
archive_sha: ${{ steps.build.outputs.archive_sha }}
image_id: ${{ steps.build.outputs.image_id }}
binary_sha: ${{ steps.build.outputs.binary_sha }}
artifact_name: ${{ steps.artifact.outputs.name }}
steps:
- name: Harden runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Check out reviewed platform source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check out pinned Dex release
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: dexidp/dex
ref: 75eb19238e10fc4750e7efb05a4031b254b04029
path: upstream
fetch-depth: 0
persist-credentials: false
- name: Set up reviewed Go compiler
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: 1.27.3
cache: false
- name: Install the static musl compiler
run: |
sudo apt-get update
sudo apt-get install --yes musl-tools
- name: Build, scan and exercise exact distribution
id: build
run: bash scripts/build-dex-security-image.sh upstream "${IMAGE}:${IMAGE_TAG}"
- name: Name the verified archive
id: artifact
run: printf 'name=dex-image-%s-%s\n' "${GITHUB_RUN_ID:?}" "${GITHUB_RUN_ATTEMPT:?}" >> "${GITHUB_OUTPUT:?}"
- name: Retain the verified image without rebuilding
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ steps.artifact.outputs.name }}
path: ${{ runner.temp }}/dex-image.tar
if-no-files-found: error
retention-days: 7
compression-level: 0

publish:
name: Publish verified Dex bytes
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: verify
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read # Check out reviewed source and its verified tool installer.
packages: write # Push the verified distribution and attestations to GHCR.
id-token: write # Obtain the main-workflow identity for keyless Cosign signing.
steps:
- name: Harden runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Check out reviewed platform source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download this run's verified image
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: ${{ needs.verify.outputs.artifact_name }}
path: ${{ runner.temp }}/dex-verified
- name: Check archive and native image identity
env:
ARCHIVE_SHA: ${{ needs.verify.outputs.archive_sha }}
IMAGE_ID: ${{ needs.verify.outputs.image_id }}
run: |
set -euo pipefail
[[ "${ARCHIVE_SHA}" =~ ^[0-9a-f]{64}$ ]]
[[ "${IMAGE_ID}" =~ ^sha256:[0-9a-f]{64}$ ]]
printf '%s %s\n' "${ARCHIVE_SHA}" "${RUNNER_TEMP}/dex-verified/dex-image.tar" | sha256sum --check
docker load --input "${RUNNER_TEMP}/dex-verified/dex-image.tar"
test "$(docker image inspect --format '{{.Id}}' "${IMAGE}:${IMAGE_TAG}")" = "${IMAGE_ID}"
- name: Install checksum-verified supply-chain tools
run: bash .github/scripts/setup-supply-chain-tools.sh
- name: Inventory the verified distribution
run: syft "docker:${IMAGE}:${IMAGE_TAG}" --output "cyclonedx-json=${RUNNER_TEMP}/dex-sbom.json"
- name: Log in to GHCR
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push, sign and verify this distribution
env:
ARCHIVE_SHA: ${{ needs.verify.outputs.archive_sha }}
IMAGE_ID: ${{ needs.verify.outputs.image_id }}
BINARY_SHA: ${{ needs.verify.outputs.binary_sha }}
run: |
set -euo pipefail
[[ "${BINARY_SHA}" =~ ^[0-9a-f]{64}$ ]]
docker push "${IMAGE}:${IMAGE_TAG}"
digest_ref="$(docker image inspect --format '{{json .RepoDigests}}' "${IMAGE}:${IMAGE_TAG}" | jq -er --arg prefix "${IMAGE}@sha256:" 'map(select(startswith($prefix))) | if length == 1 then .[0] else error("ambiguous digest") end')"
cosign sign --yes "${digest_ref}"
jq -n --arg revision "${GITHUB_SHA}" --arg imageId "${IMAGE_ID}" --arg binarySha "${BINARY_SHA}" \
--arg archiveSha "${ARCHIVE_SHA}" --arg run "${GITHUB_RUN_ID}" \
'{sourceRevision:$revision,upstreamRevision:"75eb19238e10fc4750e7efb05a4031b254b04029",patchedSourceTree:"6ba6ad659479412f2a33c391e6c64a3cfcc3f05c",imageId:$imageId,binarySha256:$binarySha,verifiedArchiveSha256:$archiveSha,workflowRunId:$run}' \
> "${RUNNER_TEMP}/dex-build.json"
cosign attest --yes --predicate "${RUNNER_TEMP}/dex-build.json" --type https://devantler.tech/attestations/dex-build/v1 "${digest_ref}"
cosign attest --yes --predicate "${RUNNER_TEMP}/dex-sbom.json" --type cyclonedx "${digest_ref}"
identity=https://github.com/devantler-tech/platform/.github/workflows/publish-dex-security-image.yaml@refs/heads/main
cosign verify --certificate-identity "${identity}" --certificate-oidc-issuer https://token.actions.githubusercontent.com "${digest_ref}"
for type in https://devantler.tech/attestations/dex-build/v1 cyclonedx; do
cosign verify-attestation --type "${type}" --certificate-identity "${identity}" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com "${digest_ref}" > /dev/null
done
printf 'Published and verified: %s\n' "${digest_ref}"
11 changes: 11 additions & 0 deletions k8s/bases/infrastructure/controllers/dex/DexSecurity.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# The native executable is compiled, scanned and exercised before this step.
# Preserve SQLite support with a static CGO build; omit the unused gomplate
# launcher. Helm's existing [dex, serve, ...] arguments execute directly.
FROM gcr.io/distroless/static-debian13:nonroot@sha256:e2e927ec666bae08560abb3c55d0659eceabb657f56b6782ab500a9fc7f555e3
COPY --chmod=0755 dist/dex /usr/local/bin/dex
COPY web /srv/dex/web
COPY dist/licenses /usr/share/licenses/dex
ENV PATH=/usr/local/bin
USER 65534:65534
ENTRYPOINT []
CMD ["dex", "serve", "/etc/dex/config.yaml"]
18 changes: 18 additions & 0 deletions k8s/bases/infrastructure/controllers/dex/IMAGE-NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Dex v2.46.1, upstream revision 75eb19238e10fc4750e7efb05a4031b254b04029
https://github.com/dexidp/dex

The Platform distribution updates golang.org/x/net from v0.59.0 to v0.60.0.
The source patch and build procedure are in devantler-tech/platform.
It retains Dex's compiled connectors and storage backends, including SQLite.
The image launches Dex directly: gomplate templates and the upstream container
entrypoint are not included. Supply an already-rendered native Dex configuration.
Dex's own environment expansion remains available.

Dex and its bundled web files retain the upstream Apache-2.0 license. Third-party
dependency license and notice files copied by Go's vendor operation are included
alongside the upstream license. source.tar.gz supplies the complete patched Dex
and vendored dependency sources, including MPL-covered files. To rebuild Dex,
use Go 1.27.3, CGO_ENABLED=1, CC=musl-gcc and the netgo,osusergo build tags with
the static external-link options in the published Platform build procedure.
Go and musl notices are included; the runtime base retains its own notices.
This modified image is maintained by Devantler Tech, not by the Dex project.
40 changes: 40 additions & 0 deletions k8s/bases/infrastructure/controllers/dex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,46 @@ A federated OpenID Connect provider for Kubernetes.
- [Documentation](https://dexidp.io/docs/)
- [Helm Chart](https://github.com/dexidp/helm-charts/tree/master/charts/dex)

## Source-built distribution

[`publish-dex-security-image.yaml`](../../../../../.github/workflows/publish-dex-security-image.yaml)
builds a reviewed Dex release with the narrow dependency correction in
[`dex-v2.46.1-network.patch`](dex-v2.46.1-network.patch). It verifies the exact
upstream commit and patched tree, runs the upstream command, server, memory-store
and OIDC connector tests, then compiles a static executable with SQLite support.
The runtime contains that musl-linked executable, upstream web files, CA
certificates, license notices and a passive source archive covering the patched
upstream and vendored dependencies. It accepts native Dex configuration and launches directly;
gomplate templates are outside this distribution's contract.

The admission command runs the pinned scanner on the actual executable itself.
Missing module coverage or a new reachable finding refuses publication. One
historical advisory has an open-ended Go version mapping but an upstream custom
range fixed before this release. Its disposition requires unchanged primary
metadata, the fixing commit's ancestry, the exact patched executable's build
information, and real HTTPS **and** gRPC handshakes. Configured TLS 1.3 must work
before and after an observed renewal on both endpoints; TLS 1.2, a wrong CA and a wrong hostname must
fail. A real TLS-1.2-configured negative control must fail admission. This is a
specific fix-or-refute decision, not a scanner suppression or a claim that all
required modules are free of advisories.

Renewal writes a new certificate with a distinct issuer and serial, waits until
both endpoints serve that leaf, and repeats the controls with its new trust
pool. A deliberately non-shipping reloader mutation must fail that observation.
The immutable archive name comes from its producing job's output, so a partial
publisher retry retains the successful verification attempt's artifact.

CI also extracts and rechecks the packaged executable and web files. Publication
loads the same archive produced by verification, checks both its hash and image
identity, and pushes those bytes without rebuilding. The main-only workflow
signs the resulting digest and attaches verified build and CycloneDX statements.
Image tags identify the source revision; consumers must use the verified digest.

Building and publishing the image does not adopt it. Runtime use requires a
separate reviewed digest pin, signer/node-pull admission and observed rollout.
The infrastructure broker and any customer broker retain independent
configuration, clients, credentials and storage.

## Sign-in pages

Dex's pages carry the Devantler Tech look: the green orb logo, the devantler.tech
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
diff --git a/go.mod b/go.mod
index 6a8a2b7..55de1c4 100644
--- a/go.mod
+++ b/go.mod
@@ -43 +43 @@ require (
- golang.org/x/net v0.59.0
+ golang.org/x/net v0.60.0
diff --git a/go.sum b/go.sum
index b02875b..13f574b 100644
--- a/go.sum
+++ b/go.sum
@@ -332,0 +333,2 @@ golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
+golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
+golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
100 changes: 100 additions & 0 deletions scripts/build-dex-security-image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -euo pipefail

root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source_dir="${1:?usage: build-dex-security-image.sh upstream-directory image-tag}"
image="${2:?usage: build-dex-security-image.sh upstream-directory image-tag}"
source_dir="$(cd "$source_dir" && pwd)"
[[ $# == 2 && $(uname -s) == Linux && $(uname -m) == x86_64 ]]
[[ $(go env GOVERSION) == go1.27.3 ]]
[[ $(git -C "$source_dir" rev-parse HEAD) == 75eb19238e10fc4750e7efb05a4031b254b04029 ]]
[[ -z $(git -C "$source_dir" status --porcelain) ]]
git -C "$source_dir" merge-base --is-ancestor 5bbdb4420254ba73b9c4df4775fe7bdacf233b17 HEAD
git -C "$source_dir" apply --check --unidiff-zero "$root/k8s/bases/infrastructure/controllers/dex/dex-v2.46.1-network.patch"
git -C "$source_dir" apply --unidiff-zero "$root/k8s/bases/infrastructure/controllers/dex/dex-v2.46.1-network.patch"
git -C "$source_dir" diff --check
git -C "$source_dir" add -- go.mod go.sum
[[ $(git -C "$source_dir" write-tree) == 6ba6ad659479412f2a33c391e6c64a3cfcc3f05c ]]

export GOTOOLCHAIN=local GOWORK=off
export GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org GOVCS='*:off'
export GOPRIVATE='' GONOPROXY='' GONOSUMDB=''
CGO_ENABLED=1 go -C "$source_dir" test -mod=readonly -timeout=5m ./cmd/dex ./server ./storage/memory ./connector/oidc
git -C "$source_dir" diff --exit-code
[[ -z $(git -C "$source_dir" ls-files --others --exclude-standard) ]]
# A controlled non-shipping executable keeps serving its original certificate.
# The renewal control must reject it; sending a signal alone used to pass.
original_serve="${RUNNER_TEMP:?}/dex-serve-original.go"
cp "$source_dir/cmd/dex/serve.go" "$original_serve"
trap 'cp "$original_serve" "$source_dir/cmd/dex/serve.go"' EXIT
[[ $(grep -Fc 'ptr.Store(loaded)' "$original_serve") == 1 ]]
sed 's/ptr.Store(loaded)/_ = loaded/' "$original_serve" > "$source_dir/cmd/dex/serve.go"
CC=musl-gcc CGO_ENABLED=1 go -C "$source_dir" build -mod=readonly -trimpath -tags netgo,osusergo \
-ldflags '-X main.version=v2.46.1-devantler.1 -linkmode external -extldflags "-static"' \
-o "$RUNNER_TEMP/dex-no-reload" ./cmd/dex
cp "$original_serve" "$source_dir/cmd/dex/serve.go"
trap - EXIT
git -C "$source_dir" diff --exit-code
mkdir "$source_dir/dist"
# Use musl's static runtime, preserving SQLite without introducing glibc into
# the distribution. Go handles network and user lookups directly.
CC=musl-gcc CGO_ENABLED=1 go -C "$source_dir" build -mod=readonly -trimpath -tags netgo,osusergo \
-ldflags '-X main.version=v2.46.1-devantler.1 -linkmode external -extldflags "-static"' \
-o "$source_dir/dist/dex" ./cmd/dex
file "$source_dir/dist/dex" | grep -q 'statically linked'
mkdir -p "${RUNNER_TEMP:?}/dex-tools"
GOBIN="$RUNNER_TEMP/dex-tools" go install golang.org/x/vuln/cmd/govulncheck@v1.8.0
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \
https://vuln.go.dev/ID/GO-2024-2476.json --output "$RUNNER_TEMP/dex-advisory.json"
DEX_NATIVE_BINARY="$source_dir/dist/dex" DEX_STALE_CERTIFICATE_BINARY="$RUNNER_TEMP/dex-no-reload" DEX_NATIVE_WEB="$source_dir/web" \
go -C "$root" test -count=1 ./scripts/check-dex-binary
go -C "$root" run ./scripts/check-dex-binary --binary "$source_dir/dist/dex" \
--scanner "$RUNNER_TEMP/dex-tools/govulncheck" --advisory "$RUNNER_TEMP/dex-advisory.json" --web "$source_dir/web"

# Go copies dependency notices when vendoring. Keep only those documents in the
# distribution; no dependency source, tests, credentials or build caches are shipped.
go -C "$source_dir" mod vendor
mkdir -p "$source_dir/dist/licenses"
cp "$source_dir/LICENSE" "$source_dir/dist/licenses/LICENSE"
cp "$root/k8s/bases/infrastructure/controllers/dex/IMAGE-NOTICE" "$source_dir/dist/licenses/NOTICE"
cp "$(go env GOROOT)/LICENSE" "$source_dir/dist/licenses/GO-LICENSE"
cp "$(go env GOROOT)/PATENTS" "$source_dir/dist/licenses/GO-PATENTS"
cp /usr/share/doc/musl/copyright "$source_dir/dist/licenses/MUSL-COPYRIGHT"
cp -L /usr/share/doc/libgcc-s1/copyright "$source_dir/dist/licenses/GCC-RUNTIME-COPYRIGHT"
cp /usr/share/common-licenses/GPL-3 "$source_dir/dist/licenses/GPL-3"
cp /usr/share/common-licenses/LGPL-3 "$source_dir/dist/licenses/LGPL-3"
cp /usr/share/common-licenses/LGPL-2.1 "$source_dir/dist/licenses/LGPL-2.1"
(
cd "$source_dir"
find vendor -type f \( -iname 'license*' -o -iname 'copying*' -o -iname 'notice*' -o -iname 'copyright*' -o -iname 'authors*' \) -print0 |
xargs -0 -r cp --parents --target-directory=dist/licenses
)
[[ $(find "$source_dir/dist/licenses/vendor" -type f | wc -l) -gt 0 ]]
# Supply the complete patched upstream and vendored dependency sources, including
# MPL-covered dependency files, alongside their notices. This is passive data;
# no source helper or second executable is introduced into the runtime.
tar --exclude=./.git --exclude=./dist --exclude=./bin --exclude=./tmp \
-czf "$source_dir/dist/licenses/source.tar.gz" -C "$source_dir" .
mkdir "$RUNNER_TEMP/dex-context"
cp -R "$source_dir/dist" "$source_dir/web" "$RUNNER_TEMP/dex-context/"
docker build --file "$root/k8s/bases/infrastructure/controllers/dex/DexSecurity.Dockerfile" \
--label org.opencontainers.image.source=https://github.com/devantler-tech/platform \
--label "org.opencontainers.image.revision=${GITHUB_SHA:?}" \
--label io.devantler.dex.upstream-revision=75eb19238e10fc4750e7efb05a4031b254b04029 \
--tag "$image" "$RUNNER_TEMP/dex-context"
docker image inspect "$image" | jq -e 'length == 1 and (.[0] | .Os == "linux" and .Architecture == "amd64" and .Config.User == "65534:65534" and ((.Config.Entrypoint // []) | length == 0))' > /dev/null
docker run --rm --read-only --cap-drop=ALL --security-opt=no-new-privileges "$image" dex version

# Inspect the packaged bytes, not just the pre-image build directory.
container_id="$(docker create "$image")"
trap 'docker rm "$container_id" >/dev/null' EXIT
mkdir "$RUNNER_TEMP/dex-packaged"
docker cp "$container_id:/usr/local/bin/dex" "$RUNNER_TEMP/dex-packaged/dex"
docker cp "$container_id:/srv/dex/web" "$RUNNER_TEMP/dex-packaged/web"
cmp "$source_dir/dist/dex" "$RUNNER_TEMP/dex-packaged/dex"
go -C "$root" run ./scripts/check-dex-binary --binary "$RUNNER_TEMP/dex-packaged/dex" \
--scanner "$RUNNER_TEMP/dex-tools/govulncheck" --advisory "$RUNNER_TEMP/dex-advisory.json" --web "$RUNNER_TEMP/dex-packaged/web"
docker save --output "$RUNNER_TEMP/dex-image.tar" "$image"
sha256sum "$RUNNER_TEMP/dex-image.tar" | awk '{print "archive_sha=" $1}' >> "${GITHUB_OUTPUT:?}"
docker image inspect --format 'image_id={{.Id}}' "$image" >> "$GITHUB_OUTPUT"
sha256sum "$source_dir/dist/dex" | awk '{print "binary_sha=" $1}' >> "$GITHUB_OUTPUT"
Loading
Loading