Skip to content

[CEL-1513] Present a client certificate to the shared buildkitd (merge before crossplane-gcloud#78) - #21

Merged
mong-x merged 1 commit into
mainfrom
marcus/cel-1513-buildkit-client
Aug 28, 2026
Merged

[CEL-1513] Present a client certificate to the shared buildkitd (merge before crossplane-gcloud#78)#21
mong-x merged 1 commit into
mainfrom
marcus/cel-1513-buildkit-client

Conversation

@mong-x

@mong-x mong-x commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Important

MERGE ORDER: this PR goes FIRST — before CellarNode/crossplane-gcloud#78.
It is step 1 of 3 in the buildkitd mTLS cutover and is written to be correct
against the current, plaintext server. #78 is step 2 and must not merge
until this is on main. Merging them in the other order breaks every
same-repo backend build until this lands.

  1. This PR — try mTLS, fall back to plaintext. Safe today: the client
    certificate does not exist in the cluster yet, so every job takes the
    fallback and behaves exactly as it does on main.
  2. crossplane-gcloud#78 — ArgoCD syncs the client Secret into
    arc-runners and flips buildkitd to TLS-only.
  3. Follow-up here — delete the fallback, so a server-side TLS regression
    fails instead of silently downgrading CI to plaintext.

Part of CEL-1513. Server half: CellarNode/crossplane-gcloud#78 (runbook:
docs/buildkit-mtls.md there).

Problem

deploy-backend.yaml connects every same-repo CellarNode/* build to the
shared in-cluster buildkitd over plain TCP:

docker buildx create --name k8s-buildkit --driver remote tcp://buildkitd.buildkit.svc.cluster.local:1234 --use

buildkitd has no authorization layer, so reachability is authorization —
anything that can open that socket runs arbitrary build steps against the
shared layer cache. #78 puts the daemon into RequireAndVerifyClientCert.
This PR is the client that has to present a certificate once it does.

Why the cutover needs a compatible first step

buildkitd applies TLS per listener, and one daemon cannot serve plaintext
and TLS on the same TCP port. The server flip is therefore atomic — and it
lives in a different repository, so there is no single commit that moves both
sides. #78's runbook resolves that with a three-step order, and step 1 is this
PR: a client that works against either server.

That gives it four states to be correct in, all four exercised below:

certificate mounted server behaviour
no plaintext today. Plaintext create, bootstrapped. Unchanged from main.
yes TLS after #78 syncs. mTLS only; the plaintext branch is never reached.
yes plaintext mid-sync, github-runners synced first. mTLS attempted, warns, falls back. Green.
no TLS mid-sync, buildkitd synced first. Fails at this step, not at the first build. Re-runnable.

What changed

.github/workflows/deploy-backend.yaml — the "Connect to in-cluster
buildkitd" step. The mTLS invocation is exactly the client contract #78
documents, with cert-manager's key names and absolute paths:

docker buildx create --name k8s-buildkit --driver remote \
  --driver-opt cacert=/run/buildkit/certs/ca.crt \
  --driver-opt cert=/run/buildkit/certs/tls.crt \
  --driver-opt key=/run/buildkit/certs/tls.key \
  tcp://buildkitd.buildkit.svc.cluster.local:1234 --use

No servername override: the server leaf's SAN list covers the FQDN dialled.
The endpoint and the certificate directory are passed as env: data rather
than inlined.

Two details that are load-bearing rather than stylistic:

  • The branch is keyed on inspect --bootstrap, not on create.
    docker buildx create only records an endpoint — it never dials the daemon,
    so it exits 0 against a server it could never handshake with. Bootstrap is
    the first thing that actually connects. Keying the fallback on create
    would mean the mTLS builder always "succeeds" and the first
    docker buildx build fails instead.
  • The fallback bootstraps too. Otherwise the fourth row above (certificate
    missing, server already TLS-only) would defer a pure connectivity failure to
    the first build step, where it reads as a build error rather than as "re-run
    this job".

.github/tests/buildkit-mtls-client.test.rb (new) — the client half of
the invariant; #78's scripts/verify_buildkit_mtls.py is the server half, and
the control only exists if both hold. It asserts the remote driver, all three
--driver-opt paths under cert-manager's key names, that the TLS branch is
gated on certificate presence and nothing else, that the connection is proven
by bootstrap, and that fork PRs stay on GitHub-hosted runners with no path to
the credential.

Its PLAINTEXT_FALLBACK_EXPECTED constant is the merge-order gate. true
here asserts the fallback is present and that the mTLS attempt precedes it;
step 3 flips it to false, at which point the test refuses any branch that
reaches buildkitd without a client certificate. The gate was verified in both
directions (below), so it is not decorative.

The regression this exists for is a silent downgrade. Dropping the
--driver-opt flags, dead-coding the TLS branch, or leaving the fallback in
place after the server is TLS-only all leave every build green while the
connection reverts to unauthenticated plaintext. Nothing else in CI notices.

.github/workflows/validate-backend-deploy.yaml (new) — runs that test on
PRs touching deploy-backend.yaml. A new workflow rather than a job on
validate-static-deploy.yaml, whose name and path filters are static-deploy
specific; this avoids renaming an existing check.

Verification

$ actionlint                                            # clean, exit 0
$ shellcheck -s bash <extracted connect step>           # clean, exit 0
$ ruby .github/tests/buildkit-mtls-client.test.rb
BuildKit mTLS client contract passed (plaintext fallback: true)
$ for t in .github/tests/*.test.rb; do ruby "$t"; done   # all 4 pass

Behavioural dry-run. The step was extracted and run against a stubbed
docker for each of the four states in the table, asserting on which builder
got created and whether the process exited 0:

state=no-certs/plain exit=0  [PLAIN → bootstrap ok]
state=certs/tls      exit=0  [MTLS  → bootstrap ok]                       (no plaintext attempt)
state=certs/plain    exit=0  [MTLS  → bootstrap fail, PLAIN → ok]         (::warning emitted)
state=no-certs/tls   exit=1  [PLAIN → bootstrap fail]                     (fails here, not at build)

Sabotage coverage. 18 mutations, each breaking one link, all caught:

caught  drop --driver-opt cacert / cert / key            (3)
caught  rename a cert key file (tls.crt → client.crt)
caught  drop inspect --bootstrap
caught  inline the endpoint literal instead of env data
caught  drop BUILDKITD_ADDR / BUILDKITD_CERT_DIR env     (2)
caught  drop a readability guard (ca.crt / tls.key)      (2)
caught  drop --driver remote
caught  rename the connect step
caught  dead-code the TLS branch (if false && [ -r ... ])
caught  append a kill-switch conjunct to the TLS guard
caught  remove the fallback bootstrap
caught  remove the fallback marker / the connected guard (2)
caught  move the fork build onto self-hosted-k8s

The last four rows are the ones worth the file existing. Three inverse tests
(clean tree, comment-only edit, unrelated step added) pass, so it is not
asserting on incidental text. Stripping every abort turns all 18 green,
so the suite is not inert.

Merge-order gate, both directions:

FAIL  flip to false, fallback still present   → "step 3 of 3 must delete the plaintext fallback"
PASS  flip to false + step-3-shaped workflow
FAIL  keep true + step-3-shaped workflow      → "step 1 of 3 must keep the plaintext fallback"

Out of scope

deploy-backend.yaml is the only workflow that touches the in-cluster
buildkitd endpoint — deploy-static-website.yaml's three self-hosted-k8s
jobs build no images. Fork PRs (build-fork) stay on GitHub-hosted runners
with no cluster access and never see a client certificate; the new test pins
that. The registry cache and the same-repo/fork trust split are untouched.

🤖 Generated with Claude Code


Summary by cubic

Makes the shared buildkitd connection in deploy-backend.yaml use mutual TLS when a client certificate is mounted into the job pod, with a plaintext fallback until the server flips. This is step 1 of 3 in the CEL-1513 cutover: it must land before crossplane-gcloud#78 makes buildkitd TLS-only, and step 3 removes the fallback.

Key details

  • Both paths prove the connection with docker buildx inspect --bootstrap, since create never dials the daemon and exits 0 even against a server it can't handshake with.
  • Mid-sync in either order, a failed handshake falls through to the other path, and a total failure fails here instead of at the first docker buildx build.
  • Adds .github/tests/buildkit-mtls-client.test.rb pinning the client contract; its PLAINTEXT_FALLBACK_EXPECTED constant is the merge-order gate, flipped to false in step 3.
  • Adds validate-backend-deploy.yaml to run that test when deploy-backend.yaml changes.
  • Fork PRs stay on GitHub-hosted runners and never see the cluster-mounted credential.

Written for commit 599a4af. Summary will update on new commits.

Review in cubic

…1513)

Step 1 of 3 of the shared buildkitd mTLS cutover. The server half is
CellarNode/crossplane-gcloud#78; its runbook is docs/buildkit-mtls.md there.

buildkitd applies TLS per listener and one daemon cannot serve plaintext and
TLS on the same TCP port, so the server cutover is atomic and spans two repos.
This lands first and is correct on both sides of it: the TLS builder is used
when the ARC container-hook extension has mounted the client leaf at
/run/buildkit/certs, and the current plaintext create runs otherwise.

`docker buildx create` records an endpoint without dialling it, so both paths
are keyed on `inspect --bootstrap` rather than create's exit status. The
fallback also bootstraps, so a build that can reach buildkitd on neither
transport fails at this step instead of at the first `docker buildx build`.

Adds .github/tests/buildkit-mtls-client.test.rb as the client half of the
invariant (the server half is scripts/verify_buildkit_mtls.py in
crossplane-gcloud). Its PLAINTEXT_FALLBACK_EXPECTED constant is the merge-order
gate: true here, flipped to false in step 3 when the fallback is deleted. The
regression it exists for is a silent downgrade — dropping the --driver-opt
flags, dead-coding the TLS branch, or letting the fallback swallow a failed
handshake once the server is TLS-only all leave every build green while the
connection reverts to unauthenticated plaintext.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0674a73d-6fac-436a-9ffc-b5de61bb1cf5

📥 Commits

Reviewing files that changed from the base of the PR and between c836dc2 and 599a4af.

📒 Files selected for processing (3)
  • .github/tests/buildkit-mtls-client.test.rb
  • .github/workflows/deploy-backend.yaml
  • .github/workflows/validate-backend-deploy.yaml

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added secure mutual-TLS connections to the shared build service when certificates are available.
    • Added automatic plaintext fallback when TLS credentials are unavailable or the TLS connection fails.
    • Added connection verification before builds proceed.
  • Bug Fixes

    • Builds now fail clearly when neither secure nor fallback connectivity can be established.
  • Tests

    • Added automated validation for TLS configuration, fallback behavior, and fork-build safeguards.

Walkthrough

The deploy workflow now connects to in-cluster buildkitd with conditional mTLS and plaintext fallback. A Ruby contract test validates the workflow structure. A read-only GitHub Actions workflow runs the test for relevant changes.

Changes

BuildKit mTLS integration

Layer / File(s) Summary
BuildKit connection flow
.github/workflows/deploy-backend.yaml
The build job recreates the remote driver with mTLS certificate options, bootstraps the connection, and falls back to plaintext when TLS fails.
mTLS contract validation
.github/tests/buildkit-mtls-client.test.rb
The Ruby test checks certificate wiring, TLS guards, bootstrap commands, fallback ordering, and fork-job driver restrictions.
Automated workflow validation
.github/workflows/validate-backend-deploy.yaml
A read-only workflow runs the contract test for relevant pull requests and pushes to main.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🔵 Low · up to 599a4

This PR makes backend builds try mTLS first but temporarily fall back to plaintext when the handshake fails. During the staged rollout, a listener that still accepts plaintext could allow same-repository builds to proceed without client authentication, so the change is mergeable with explicit owner awareness and timely removal of the fallback before TLS-only enforcement.

Sequence Diagram(s)

sequenceDiagram
  participant BuildJob
  participant DockerBuildx
  participant Buildkitd
  BuildJob->>DockerBuildx: Recreate k8s-buildkit with mTLS options
  DockerBuildx->>Buildkitd: Run inspect --bootstrap over TLS
  BuildJob->>DockerBuildx: Recreate with plaintext fallback if TLS fails
  DockerBuildx->>Buildkitd: Run inspect --bootstrap over plaintext
Loading

Suggested labels: feature

Poem

A rabbit checks the certs in line

Three keys glow beneath the sign
Buildkit hops through TLS air
Plaintext waits as backup there
Tests keep every path in view

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: presenting a client certificate to the shared buildkitd. The merge-order note is relevant to the stated cutover plan.
Description check ✅ Passed The description directly explains the mTLS client changes, plaintext fallback, contract tests, validation workflow, and required merge order.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 1…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 1 files. (2 skipped: 2 unsupported.)

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch marcus/cel-1513-buildkit-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added the feature label Aug 28, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 3 files

Confidence score: 2/5

  • In .github/workflows/deploy-backend.yaml, failed or missing client-certificate setup falls back to recreating k8s-buildkit over tcp:// and continues, which can bypass mTLS and expose the build path; fail closed or require a secure endpoint instead.
  • In .github/tests/buildkit-mtls-client.test.rb, the assertions verify the fallback markers and ordering but not connected=true, so a missing TLS-success path could regress without detection; require the successful assignment in the test.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/tests/buildkit-mtls-client.test.rb">

<violation number="1" location=".github/tests/buildkit-mtls-client.test.rb:92">
P2: The test pins the fallback markers (`connected=false`, `if [ "$connected" != true ]; then`) and that the TLS attempt precedes the fallback, but it never requires the TLS-success assignment `connected=true` in the workflow. Deleting `connected=true` (workflow line 153) passes this test: the guard is still all three cert files, the driver-opt flags are present, and the fallback still bootstraps. On today's plaintext server the build stays green (a silent downgrade while certs are present, mid-sync), and after crossplane-gcloud#78 switches the server to TLS-only every normal build would fall through to the plaintext fallback and fail at bootstrap — the exact silent-downgrade then late-break this test exists to prevent. Add `connected=true` to the required markers so the fallback is proven gated on TLS success.</violation>
</file>

<file name=".github/workflows/deploy-backend.yaml">

<violation number="1" location=".github/workflows/deploy-backend.yaml:161">
P1: Custom agent: **Flag Security Vulnerabilities**

When the client certificates are absent or mTLS bootstrap fails, this fallback re-creates `k8s-buildkit` against the `tcp://` endpoint and continues after only a warning. That leaves the shared BuildKit connection unauthenticated; remove the plaintext fallback before relying on this workflow for the CEL-1513 hardening.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

echo "no buildkitd client certificate at ${BUILDKITD_CERT_DIR} - using plaintext (CEL-1513 step 1 of 3)"
fi

if [ "$connected" != true ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Custom agent: Flag Security Vulnerabilities

When the client certificates are absent or mTLS bootstrap fails, this fallback re-creates k8s-buildkit against the tcp:// endpoint and continues after only a warning. That leaves the shared BuildKit connection unauthenticated; remove the plaintext fallback before relying on this workflow for the CEL-1513 hardening.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/deploy-backend.yaml, line 161:

<comment>When the client certificates are absent or mTLS bootstrap fails, this fallback re-creates `k8s-buildkit` against the `tcp://` endpoint and continues after only a warning. That leaves the shared BuildKit connection unauthenticated; remove the plaintext fallback before relying on this workflow for the CEL-1513 hardening.</comment>

<file context>
@@ -94,12 +94,74 @@ jobs:
+            echo "no buildkitd client certificate at ${BUILDKITD_CERT_DIR} - using plaintext (CEL-1513 step 1 of 3)"
+          fi
+
+          if [ "$connected" != true ]; then
+            recreate
+            docker buildx inspect --bootstrap k8s-buildkit
</file context>

Comment on lines +92 to +95
fallback_markers = [
"connected=false",
%(if [ "$connected" != true ]; then),
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The test pins the fallback markers (connected=false, if [ "$connected" != true ]; then) and that the TLS attempt precedes the fallback, but it never requires the TLS-success assignment connected=true in the workflow. Deleting connected=true (workflow line 153) passes this test: the guard is still all three cert files, the driver-opt flags are present, and the fallback still bootstraps. On today's plaintext server the build stays green (a silent downgrade while certs are present, mid-sync), and after crossplane-gcloud#78 switches the server to TLS-only every normal build would fall through to the plaintext fallback and fail at bootstrap — the exact silent-downgrade then late-break this test exists to prevent. Add connected=true to the required markers so the fallback is proven gated on TLS success.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/tests/buildkit-mtls-client.test.rb, line 92:

<comment>The test pins the fallback markers (`connected=false`, `if [ "$connected" != true ]; then`) and that the TLS attempt precedes the fallback, but it never requires the TLS-success assignment `connected=true` in the workflow. Deleting `connected=true` (workflow line 153) passes this test: the guard is still all three cert files, the driver-opt flags are present, and the fallback still bootstraps. On today's plaintext server the build stays green (a silent downgrade while certs are present, mid-sync), and after crossplane-gcloud#78 switches the server to TLS-only every normal build would fall through to the plaintext fallback and fail at bootstrap — the exact silent-downgrade then late-break this test exists to prevent. Add `connected=true` to the required markers so the fallback is proven gated on TLS success.</comment>

<file context>
@@ -0,0 +1,132 @@
+
+# --- merge-order gate --------------------------------------------------------
+
+fallback_markers = [
+  "connected=false",
+  %(if [ "$connected" != true ]; then),
</file context>
Suggested change
fallback_markers = [
"connected=false",
%(if [ "$connected" != true ]; then),
]
fallback_markers = [
&quot;connected=false&quot;,
&quot;connected=true&quot;,
%(if [ &quot;$connected&quot; != true ]; then),
]

@mong-x
mong-x merged commit e33b072 into main Aug 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant