Skip to content

Commit 32550e2

Browse files
ktdreyerclaude
andcommitted
mount trusted CA bundle in runner pods
On clusters with private or corporate CAs, runner pods fail TLS validation because they only have the default UBI9 system CA store. The "backend-api" Deployment already supports a trusted CA bundle, but runner pods (created dynamically by the operator) never had matching support. "applyTrustedCABundle" checks for a "trusted-ca-bundle" ConfigMap in the session namespace before each pod is created. On OpenShift, this ConfigMap is auto-populated by CA injection; on other clusters, operators can provision it manually. When present, the ConfigMap is mounted at "/etc/pki/tls/certs/ca-bundle.crt" (the UBI9 system CA path) so all TLS clients in the runner container trust the corporate CA without any application-level changes. Clusters without the ConfigMap are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4c8486e commit 32550e2

File tree

3 files changed

+246
-0
lines changed

3 files changed

+246
-0
lines changed

components/operator/internal/handlers/sessions.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2626
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2727
intstr "k8s.io/apimachinery/pkg/util/intstr"
28+
"k8s.io/client-go/kubernetes"
2829
"k8s.io/client-go/util/retry"
2930
)
3031

@@ -1313,6 +1314,9 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error {
13131314
}
13141315
}
13151316

1317+
// Mount trusted CA bundle if present in the session namespace (e.g. OpenShift CA injection)
1318+
applyTrustedCABundle(config.K8sClient, sessionNamespace, pod)
1319+
13161320
// NOTE: Google credentials are now fetched at runtime via backend API
13171321
// No longer mounting credentials.json as volume
13181322
// This ensures tokens are always fresh and automatically refreshed
@@ -2161,6 +2165,55 @@ func deleteAmbientLangfuseSecret(ctx context.Context, namespace string) error {
21612165
return nil
21622166
}
21632167

2168+
// applyTrustedCABundle mounts the cluster's injected CA bundle into the runner
2169+
// container so it trusts cluster-internal TLS certificates (e.g. on OpenShift).
2170+
// Clusters without the ConfigMap are silently unaffected.
2171+
func applyTrustedCABundle(k8sClient kubernetes.Interface, namespace string, pod *corev1.Pod) {
2172+
cm, err := k8sClient.CoreV1().ConfigMaps(namespace).Get(
2173+
context.TODO(), types.TrustedCABundleConfigMapName, v1.GetOptions{})
2174+
if errors.IsNotFound(err) {
2175+
return
2176+
}
2177+
if err != nil {
2178+
log.Printf("Warning: failed to check for %s ConfigMap in %s: %v",
2179+
types.TrustedCABundleConfigMapName, namespace, err)
2180+
return
2181+
}
2182+
if _, ok := cm.Data["ca-bundle.crt"]; !ok {
2183+
if _, ok := cm.BinaryData["ca-bundle.crt"]; !ok {
2184+
log.Printf("Warning: %s ConfigMap in %s is missing required key ca-bundle.crt; skipping mount",
2185+
types.TrustedCABundleConfigMapName, namespace)
2186+
return
2187+
}
2188+
}
2189+
pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
2190+
Name: "trusted-ca-bundle",
2191+
VolumeSource: corev1.VolumeSource{
2192+
ConfigMap: &corev1.ConfigMapVolumeSource{
2193+
LocalObjectReference: corev1.LocalObjectReference{
2194+
Name: types.TrustedCABundleConfigMapName,
2195+
},
2196+
},
2197+
},
2198+
})
2199+
for i := range pod.Spec.Containers {
2200+
if pod.Spec.Containers[i].Name == "ambient-code-runner" {
2201+
pod.Spec.Containers[i].VolumeMounts = append(
2202+
pod.Spec.Containers[i].VolumeMounts,
2203+
corev1.VolumeMount{
2204+
Name: "trusted-ca-bundle",
2205+
MountPath: "/etc/pki/tls/certs/ca-bundle.crt",
2206+
SubPath: "ca-bundle.crt",
2207+
ReadOnly: true,
2208+
},
2209+
)
2210+
log.Printf("Mounted %s ConfigMap to /etc/pki/tls/certs/ca-bundle.crt in runner container",
2211+
types.TrustedCABundleConfigMapName)
2212+
break
2213+
}
2214+
}
2215+
}
2216+
21642217
// LEGACY: getBackendAPIURL removed - AG-UI migration
21652218
// Workflow and repo changes now call runner's REST endpoints directly
21662219

components/operator/internal/handlers/sessions_test.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package handlers
22

33
import (
44
"context"
5+
"fmt"
56
"testing"
67

78
"ambient-code-operator/internal/config"
@@ -13,6 +14,7 @@ import (
1314
"k8s.io/apimachinery/pkg/runtime"
1415
k8stypes "k8s.io/apimachinery/pkg/types"
1516
"k8s.io/client-go/kubernetes/fake"
17+
clienttesting "k8s.io/client-go/testing"
1618
)
1719

1820
// setupTestClient initializes a fake Kubernetes client for testing
@@ -568,6 +570,193 @@ func TestDeleteAmbientVertexSecret_NotFound(t *testing.T) {
568570
}
569571
}
570572

573+
// TestApplyTrustedCABundle_ConfigMapPresent verifies that applyTrustedCABundle adds the volume
574+
// and VolumeMount when the trusted-ca-bundle ConfigMap exists in the session namespace.
575+
func TestApplyTrustedCABundle_ConfigMapPresent(t *testing.T) {
576+
cm := &corev1.ConfigMap{
577+
ObjectMeta: metav1.ObjectMeta{
578+
Name: types.TrustedCABundleConfigMapName,
579+
Namespace: "session-ns",
580+
},
581+
Data: map[string]string{
582+
"ca-bundle.crt": "--- fake CA data ---",
583+
},
584+
}
585+
setupTestClient(cm)
586+
587+
pod := &corev1.Pod{
588+
Spec: corev1.PodSpec{
589+
Containers: []corev1.Container{
590+
{Name: "ambient-code-runner"},
591+
},
592+
},
593+
}
594+
595+
applyTrustedCABundle(config.K8sClient, "session-ns", pod)
596+
597+
if len(pod.Spec.Volumes) != 1 {
598+
t.Fatalf("expected 1 volume, got %d", len(pod.Spec.Volumes))
599+
}
600+
vol := pod.Spec.Volumes[0]
601+
if vol.Name != "trusted-ca-bundle" {
602+
t.Errorf("expected volume name 'trusted-ca-bundle', got %q", vol.Name)
603+
}
604+
if vol.ConfigMap == nil || vol.ConfigMap.Name != types.TrustedCABundleConfigMapName {
605+
t.Errorf("expected ConfigMap volume sourced from %q", types.TrustedCABundleConfigMapName)
606+
}
607+
608+
mounts := pod.Spec.Containers[0].VolumeMounts
609+
if len(mounts) != 1 {
610+
t.Fatalf("expected 1 VolumeMount, got %d", len(mounts))
611+
}
612+
m := mounts[0]
613+
if m.Name != "trusted-ca-bundle" {
614+
t.Errorf("expected mount name 'trusted-ca-bundle', got %q", m.Name)
615+
}
616+
if m.MountPath != "/etc/pki/tls/certs/ca-bundle.crt" {
617+
t.Errorf("unexpected MountPath: %q", m.MountPath)
618+
}
619+
if m.SubPath != "ca-bundle.crt" {
620+
t.Errorf("expected SubPath 'ca-bundle.crt', got %q", m.SubPath)
621+
}
622+
if !m.ReadOnly {
623+
t.Error("expected ReadOnly=true")
624+
}
625+
}
626+
627+
// TestApplyTrustedCABundle_ConfigMapAbsent verifies that applyTrustedCABundle leaves the pod
628+
// unchanged when the trusted-ca-bundle ConfigMap is not present in the session namespace.
629+
func TestApplyTrustedCABundle_ConfigMapAbsent(t *testing.T) {
630+
setupTestClient() // no ConfigMap
631+
632+
pod := &corev1.Pod{
633+
Spec: corev1.PodSpec{
634+
Containers: []corev1.Container{
635+
{Name: "ambient-code-runner"},
636+
},
637+
},
638+
}
639+
640+
applyTrustedCABundle(config.K8sClient, "session-ns", pod)
641+
642+
if len(pod.Spec.Volumes) != 0 {
643+
t.Errorf("expected no volumes, got %d", len(pod.Spec.Volumes))
644+
}
645+
if len(pod.Spec.Containers[0].VolumeMounts) != 0 {
646+
t.Errorf("expected no VolumeMounts, got %d", len(pod.Spec.Containers[0].VolumeMounts))
647+
}
648+
}
649+
650+
// TestApplyTrustedCABundle_ExistingMountsPreserved verifies that applyTrustedCABundle appends
651+
// to, rather than replacing, existing VolumeMounts on the runner container.
652+
func TestApplyTrustedCABundle_ExistingMountsPreserved(t *testing.T) {
653+
cm := &corev1.ConfigMap{
654+
ObjectMeta: metav1.ObjectMeta{
655+
Name: types.TrustedCABundleConfigMapName,
656+
Namespace: "session-ns",
657+
},
658+
Data: map[string]string{
659+
"ca-bundle.crt": "--- fake CA data ---",
660+
},
661+
}
662+
setupTestClient(cm)
663+
664+
existingMount := corev1.VolumeMount{
665+
Name: "runner-token",
666+
MountPath: "/var/run/secrets/ambient",
667+
ReadOnly: true,
668+
}
669+
pod := &corev1.Pod{
670+
Spec: corev1.PodSpec{
671+
Volumes: []corev1.Volume{
672+
{Name: "runner-token"},
673+
},
674+
Containers: []corev1.Container{
675+
{
676+
Name: "ambient-code-runner",
677+
VolumeMounts: []corev1.VolumeMount{existingMount},
678+
},
679+
},
680+
},
681+
}
682+
683+
applyTrustedCABundle(config.K8sClient, "session-ns", pod)
684+
685+
if len(pod.Spec.Volumes) != 2 {
686+
t.Fatalf("expected 2 volumes (runner-token + trusted-ca-bundle), got %d", len(pod.Spec.Volumes))
687+
}
688+
mounts := pod.Spec.Containers[0].VolumeMounts
689+
if len(mounts) != 2 {
690+
t.Fatalf("expected 2 VolumeMounts, got %d", len(mounts))
691+
}
692+
// Existing mount must still be at index 0
693+
if mounts[0].Name != "runner-token" {
694+
t.Errorf("expected first mount to be 'runner-token', got %q", mounts[0].Name)
695+
}
696+
if mounts[1].Name != "trusted-ca-bundle" {
697+
t.Errorf("expected second mount to be 'trusted-ca-bundle', got %q", mounts[1].Name)
698+
}
699+
}
700+
701+
// TestApplyTrustedCABundle_MissingKey verifies that applyTrustedCABundle leaves the pod
702+
// unchanged when the ConfigMap exists but lacks the ca-bundle.crt key.
703+
func TestApplyTrustedCABundle_MissingKey(t *testing.T) {
704+
cm := &corev1.ConfigMap{
705+
ObjectMeta: metav1.ObjectMeta{
706+
Name: types.TrustedCABundleConfigMapName,
707+
Namespace: "session-ns",
708+
},
709+
Data: map[string]string{
710+
"wrong-key.pem": "--- fake CA data ---",
711+
},
712+
}
713+
setupTestClient(cm)
714+
715+
pod := &corev1.Pod{
716+
Spec: corev1.PodSpec{
717+
Containers: []corev1.Container{
718+
{Name: "ambient-code-runner"},
719+
},
720+
},
721+
}
722+
723+
applyTrustedCABundle(config.K8sClient, "session-ns", pod)
724+
725+
if len(pod.Spec.Volumes) != 0 {
726+
t.Errorf("expected no volumes when key is missing, got %d", len(pod.Spec.Volumes))
727+
}
728+
if len(pod.Spec.Containers[0].VolumeMounts) != 0 {
729+
t.Errorf("expected no VolumeMounts when key is missing, got %d", len(pod.Spec.Containers[0].VolumeMounts))
730+
}
731+
}
732+
733+
// TestApplyTrustedCABundle_APIError verifies that applyTrustedCABundle leaves the pod
734+
// unchanged when the ConfigMap GET returns a non-NotFound error.
735+
func TestApplyTrustedCABundle_APIError(t *testing.T) {
736+
fakeClient := fake.NewSimpleClientset()
737+
fakeClient.PrependReactor("get", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) {
738+
return true, nil, fmt.Errorf("connection refused")
739+
})
740+
config.K8sClient = fakeClient
741+
742+
pod := &corev1.Pod{
743+
Spec: corev1.PodSpec{
744+
Containers: []corev1.Container{
745+
{Name: "ambient-code-runner"},
746+
},
747+
},
748+
}
749+
750+
applyTrustedCABundle(config.K8sClient, "session-ns", pod)
751+
752+
if len(pod.Spec.Volumes) != 0 {
753+
t.Errorf("expected no volumes on API error, got %d", len(pod.Spec.Volumes))
754+
}
755+
if len(pod.Spec.Containers[0].VolumeMounts) != 0 {
756+
t.Errorf("expected no VolumeMounts on API error, got %d", len(pod.Spec.Containers[0].VolumeMounts))
757+
}
758+
}
759+
571760
// TestDeleteAmbientVertexSecret_NilAnnotations tests handling of secret with nil annotations
572761
func TestDeleteAmbientVertexSecret_NilAnnotations(t *testing.T) {
573762
secret := &corev1.Secret{

components/operator/internal/types/resources.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ const (
77
// AmbientVertexSecretName is the name of the secret containing Vertex AI credentials
88
AmbientVertexSecretName = "ambient-vertex"
99

10+
// TrustedCABundleConfigMapName is the CA bundle ConfigMap injected by OpenShift or provisioned
11+
// manually on private-CA clusters. See issue #1247.
12+
TrustedCABundleConfigMapName = "trusted-ca-bundle"
13+
1014
// CopiedFromAnnotation is the annotation key used to track secrets copied by the operator
1115
CopiedFromAnnotation = "vteam.ambient-code/copied-from"
1216
)

0 commit comments

Comments
 (0)