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
32 changes: 32 additions & 0 deletions e2e/internal/contrasttest/contrasttest.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
)

// Flags contains the parsed Flags for the test.
Expand Down Expand Up @@ -85,6 +86,10 @@ type ContrastTest struct {
GHCRToken string
Kubeclient *kubeclient.Kubeclient

// SkipKDSProxy disables injection of kds-proxy env vars + CA bundle into.
// Used by kds-pcs-downtime test.
SkipKDSProxy bool

// outputs of contrast subcommands
meshCACertPEM []byte
rootCACertPEM []byte
Expand Down Expand Up @@ -187,6 +192,12 @@ func (ct *ContrastTest) Init(t *testing.T, resources []any) {
resources = kuberesource.AddLogging(resources, "debug", "*")
resources = kuberesource.PatchNodeSelector(resources)
resources = ct.OverrideStorageClass(t, resources)
if !ct.SkipKDSProxy {
ct.copyKDSProxyCA(t)
resources = kuberesource.AddKDSProxy(resources,
kuberesource.KDSProxyDefaultService,
kuberesource.KDSProxyCAConfigMap)
}
unstructuredResources, err := kuberesource.ResourcesToUnstructured(resources)
require.NoError(err)

Expand All @@ -198,6 +209,27 @@ func (ct *ContrastTest) Init(t *testing.T, resources []any) {
ct.installRuntime(t, resources)
}

// copyKDSProxyCA copies the cluster-wide kds-proxy CA ConfigMap from "default"
// into the test's namespace, where the AddKDSProxy mutator mounts it.
func (ct *ContrastTest) copyKDSProxyCA(t *testing.T) {
require := require.New(t)
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
defer cancel()

src, err := ct.Kubeclient.Client.CoreV1().
ConfigMaps("default").
Get(ctx, kuberesource.KDSProxyCAConfigMap, metav1.GetOptions{})
require.NoError(err,
"kds-proxy CA ConfigMap %q missing in default namespace — run the kds-proxy bootstrap step",
kuberesource.KDSProxyCAConfigMap)

cm := corev1ac.ConfigMap(kuberesource.KDSProxyCAConfigMap, ct.Namespace).
WithData(src.Data)
unstr, err := kuberesource.ResourcesToUnstructured([]any{cm})
require.NoError(err)
require.NoError(ct.Kubeclient.Apply(ctx, unstr...))
}

// OverrideStorageClass looks for a StorageClass with a well-known label and modifies the resources to use that class.
func (ct *ContrastTest) OverrideStorageClass(t *testing.T, resources []any) []any {
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
Expand Down
2 changes: 2 additions & 0 deletions e2e/kds-pcs-downtime/kds-pcs-downtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ func TestKDSPCSDowntime(t *testing.T) {
platform, err := platforms.FromString(contrasttest.Flags.PlatformStr)
require.NoError(t, err)
ct := contrasttest.New(t)
// This test drives https_proxy itself via goproxy.
ct.SkipKDSProxy = true

runtimeHandler, err := manifest.RuntimeHandler(platform)
require.NoError(t, err)
Expand Down
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use (
./imagepuller
./imagestore
./initdata-processor
./kds-proxy
./service-mesh
./tools/debugshell
./tools/fifo
Expand Down
79 changes: 79 additions & 0 deletions internal/kuberesource/mutators.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ const (
securePVAnnotationKey = "contrast.edgeless.systems/secure-pv"
workloadSecretIDAnnotationKey = "contrast.edgeless.systems/workload-secret-id"
imageStoreSizeAnnotationKey = "contrast.edgeless.systems/image-store-size"
skipKDSProxyAnnotationKey = "contrast.edgeless.systems/skip-kds-proxy"
)

// Defaults for routing Contrast pod attestation traffic through the in-cluster kds-proxy.
const (
KDSProxyDefaultService = "http://kds-proxy.default.svc:3128"
KDSProxyCAConfigMap = "kds-proxy-ca"
KDSProxyCAKey = "ca.crt"
kdsProxyCAVolumeName = "kds-proxy-ca"
kdsProxyMountDir = "/etc/ssl/kds-proxy"
kdsProxyCAPath = kdsProxyMountDir + "/" + KDSProxyCAKey

// Keep in-cluster traffic out of the forward proxy.
kdsProxyNoProxy = "localhost,127.0.0.1,.svc,.svc.cluster.local,.cluster.local," +
"10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16"
)

// contrastRuntimeClassPrefixes lists runtime class prefixes that identify Contrast pods.
Expand Down Expand Up @@ -300,6 +315,70 @@ func ensureVolumeExists(spec *applycorev1.PodSpecApplyConfiguration, volumeName
return nil
}

// AddKDSProxy mounts the proxy CA from configMapName and uses the SSL_CERT_FILE in every Contrast container.
func AddKDSProxy(resources []any, proxyURL, configMapName string) []any {
out := make([]any, 0, len(resources))
for _, resource := range resources {
out = append(out, MapPodSpecWithMeta(resource, func(meta *applymetav1.ObjectMetaApplyConfiguration, spec *applycorev1.PodSpecApplyConfiguration) (*applymetav1.ObjectMetaApplyConfiguration, *applycorev1.PodSpecApplyConfiguration) {
if !IsContrastPod(spec) {
return meta, spec
}
if meta != nil && meta.Annotations[skipKDSProxyAnnotationKey] == "true" {
return meta, spec
}
injectKDSProxy(spec, proxyURL, configMapName)
return meta, spec
}))
}
return out
}

func injectKDSProxy(spec *applycorev1.PodSpecApplyConfiguration, proxyURL, configMapName string) {
if !hasVolumeNamed(spec, kdsProxyCAVolumeName) {
spec.Volumes = append(spec.Volumes, *applycorev1.Volume().
WithName(kdsProxyCAVolumeName).
WithConfigMap(applycorev1.ConfigMapVolumeSource().WithName(configMapName)))
}

envs := []struct{ name, value string }{
{"https_proxy", proxyURL},
{"HTTPS_PROXY", proxyURL},
{"no_proxy", kdsProxyNoProxy},
{"NO_PROXY", kdsProxyNoProxy},
{"SSL_CERT_FILE", kdsProxyCAPath},
}
for i := range spec.Containers {
c := &spec.Containers[i]
for _, e := range envs {
if !hasEnvNamed(c, e.name) {
c.Env = append(c.Env, *applycorev1.EnvVar().WithName(e.name).WithValue(e.value))
}
}
addOrReplaceVolumeMount(c, *applycorev1.VolumeMount().
WithName(kdsProxyCAVolumeName).
WithMountPath(kdsProxyMountDir).
WithReadOnly(true))
}
}

func hasVolumeNamed(spec *applycorev1.PodSpecApplyConfiguration, name string) bool {
for _, v := range spec.Volumes {
if v.Name != nil && *v.Name == name {
return true
}
}
return false
}

func hasEnvNamed(c *applycorev1.ContainerApplyConfiguration, name string) bool {
for _, e := range c.Env {
if e.Name != nil && *e.Name == name {
return true
}
}
return false
}

// AddPortForwarders adds a port-forwarder for each Service.
func AddPortForwarders(resources []any) []any {
var out []any
Expand Down
56 changes: 56 additions & 0 deletions internal/kuberesource/mutators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,62 @@ spec:
}
}

func TestAddKDSProxy(t *testing.T) {
require := require.New(t)
assert := assert.New(t)

contrastPod := applycorev1.Pod("worker", "default").
WithSpec(applycorev1.PodSpec().
WithRuntimeClassName("contrast-cc-foo").
WithContainers(applycorev1.Container().WithName("app").WithImage("nginx")))
skipPod := applycorev1.Pod("skipper", "default").
WithAnnotations(map[string]string{skipKDSProxyAnnotationKey: "true"}).
WithSpec(applycorev1.PodSpec().
WithRuntimeClassName("contrast-cc-foo").
WithContainers(applycorev1.Container().WithName("app").WithImage("nginx")))
nonContrastPod := applycorev1.Pod("plain", "default").
WithSpec(applycorev1.PodSpec().
WithContainers(applycorev1.Container().WithName("app").WithImage("nginx")))

out := AddKDSProxy([]any{contrastPod, skipPod, nonContrastPod},
"http://kds-proxy:3128", "kds-proxy-ca")
require.Len(out, 3)

mutated, ok := out[0].(*applycorev1.PodApplyConfiguration)
require.True(ok)
spec := mutated.Spec
assert.Empty(spec.InitContainers, "should not add an init container")

require.Len(spec.Containers, 1)
envs := map[string]string{}
for _, e := range spec.Containers[0].Env {
envs[*e.Name] = *e.Value
}
assert.Equal("http://kds-proxy:3128", envs["https_proxy"])
assert.Equal("http://kds-proxy:3128", envs["HTTPS_PROXY"])
assert.Equal(kdsProxyNoProxy, envs["no_proxy"])
assert.Equal(kdsProxyNoProxy, envs["NO_PROXY"])
assert.Equal(kdsProxyCAPath, envs["SSL_CERT_FILE"])
require.Len(spec.Volumes, 1)
assert.Equal(kdsProxyCAVolumeName, *spec.Volumes[0].Name)

skipped, ok := out[1].(*applycorev1.PodApplyConfiguration)
require.True(ok)
assert.Empty(skipped.Spec.Volumes)
assert.Empty(skipped.Spec.Containers[0].Env)

plain, ok := out[2].(*applycorev1.PodApplyConfiguration)
require.True(ok)
assert.Empty(plain.Spec.Volumes)
assert.Empty(plain.Spec.Containers[0].Env)

out2 := AddKDSProxy(out, "http://kds-proxy:3128", "kds-proxy-ca")
mutated2, ok := out2[0].(*applycorev1.PodApplyConfiguration)
require.True(ok)
assert.Len(mutated2.Spec.Volumes, 1)
assert.Len(mutated2.Spec.Containers[0].Env, 5)
}

func TestMapPodSpecWithErrors(t *testing.T) {
require := require.New(t)

Expand Down
55 changes: 55 additions & 0 deletions internal/kuberesource/parts.go
Original file line number Diff line number Diff line change
Expand Up @@ -573,3 +573,58 @@ func GetPodCPUCount(spec *applycorev1.PodSpecApplyConfiguration) uint64 {
totalCPUs := (totalMilliCPUs+999)/1000 + 1
return uint64(totalCPUs)
}

// KDSProxy returns the resources for an in-cluster HTTPS forward proxy caching responses from AMD KDS, Intel PCS, and NVIDIA RIM endpoints.
func KDSProxy(namespace, storageClassName string) []any {
const (
name = "kds-proxy"
port = int32(3128)
stateDir = "/var/lib/kds-proxy"
stateVol = "state"
)
labels := map[string]string{"app.kubernetes.io/name": name}

pvcSpec := applycorev1.PersistentVolumeClaimSpec().
WithAccessModes(corev1.ReadWriteOnce).
WithResources(applycorev1.VolumeResourceRequirements().
WithRequests(corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("1Gi")}))
if storageClassName != "" {
pvcSpec = pvcSpec.WithStorageClassName(storageClassName)
}
pvc := applycorev1.PersistentVolumeClaim(name+"-state", namespace).WithSpec(pvcSpec)

mem := corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("256Mi")}
deployment := Deployment(name, namespace).
WithSpec(DeploymentSpec().
WithReplicas(1).
WithSelector(LabelSelector().WithMatchLabels(labels)).
WithStrategy(applyappsv1.DeploymentStrategy().
WithType(appsv1.RecreateDeploymentStrategyType)).
WithTemplate(PodTemplateSpec().
WithLabels(labels).
WithSpec(PodSpec().
WithVolumes(applycorev1.Volume().
WithName(stateVol).
WithPersistentVolumeClaim(applycorev1.PersistentVolumeClaimVolumeSource().
WithClaimName(name + "-state"))).
WithContainers(applycorev1.Container().
WithName(name).
WithImage("ghcr.io/edgelesssys/contrast/kds-proxy:latest").
WithArgs(fmt.Sprintf("-addr=:%d", port), "-state-dir="+stateDir).
WithPorts(applycorev1.ContainerPort().
WithName("proxy").
WithContainerPort(port)).
WithVolumeMounts(applycorev1.VolumeMount().
WithName(stateVol).
WithMountPath(stateDir)).
WithReadinessProbe(applycorev1.Probe().
WithHTTPGet(applycorev1.HTTPGetAction().
WithPath("/healthz").
WithPort(intstr.FromInt32(port))).
WithPeriodSeconds(5)).
WithResources(applycorev1.ResourceRequirements().
WithRequests(mem).
WithLimits(mem))))))

return []any{deployment, ServiceForDeployment(deployment), pvc}
}
11 changes: 11 additions & 0 deletions internal/kuberesource/parts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ func TestCoordinator(t *testing.T) {
t.Log("\n" + string(b))
}

func TestKDSProxy(t *testing.T) {
require := require.New(t)

resources := KDSProxy("default", "")
require.Len(resources, 3)

b, err := EncodeResources(resources...)
require.NoError(err)
t.Log("\n" + string(b))
}

func TestNoNamespaces(t *testing.T) {
coordinator := CoordinatorBundle()
openssl := OpenSSL()
Expand Down
2 changes: 2 additions & 0 deletions internal/kuberesource/resourcegen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ func main() {
switch set {
case "coordinator":
subResources = kuberesource.PatchRuntimeHandlers(kuberesource.CoordinatorBundle(), "contrast-cc")
case "kds-proxy":
subResources = kuberesource.KDSProxy(*namespace, *storageClass)
case "runtime":
platformCollection := kuberesource.PlatformCollection{}
if err := platformCollection.AddFromCommaSeparated(*rawPlatform); err != nil {
Expand Down
34 changes: 34 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ port-forwarder: (push "port-forwarder")

service-mesh-proxy: (push "service-mesh-proxy")

kds-proxy: (push "kds-proxy")

initializer: (push "initializer")

memdump: (push "memdump")
Expand Down Expand Up @@ -429,6 +431,38 @@ wait-for-workload target=default_deploy_target set=default_set:
;;
esac

# Provision the kds-proxy singleton in "default" and its kds-proxy-ca ConfigMap. Re-run after pushing a new image.
kds-proxy-bootstrap set=default_set storage_class="": kds-proxy
#!/usr/bin/env bash
set -euo pipefail
sc="{{ storage_class }}"
if [[ -z "$sc" ]]; then
sc=$(kubectl get sc -l ci.contrast.edgeless.systems/is-default-class=true -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
fi
args=(--image-replacements ./{{ workspace_dir }}/just.containerlookup --namespace default)
if [[ -n "$sc" ]]; then
args+=(--storage-class "$sc")
fi
nix shell .#{{ set }}.contrast.resourcegen --command resourcegen "${args[@]}" kds-proxy \
| kubectl apply --server-side --force-conflicts -f -
kubectl -n default rollout status deploy/kds-proxy --timeout=120s
ca=$(kubectl -n default exec deploy/kds-proxy -- cat /var/lib/kds-proxy/ca/ca.crt)
kubectl -n default create configmap kds-proxy-ca \
--from-literal=ca.crt="$ca" \
--dry-run=client -o yaml | kubectl apply -f -

# Wipe the existing kds-proxy (pod, service, PVC, CA configmap) and bootstrap a fresh one.
kds-proxy-redeploy set=default_set storage_class="":
#!/usr/bin/env bash
set -euo pipefail
kubectl -n default delete --ignore-not-found --wait=true \
deploy/kds-proxy svc/kds-proxy pvc/kds-proxy-state configmap/kds-proxy-ca
just kds-proxy-bootstrap "{{ set }}" "{{ storage_class }}"

# Print the kds-proxy /metrics endpoint.
kds-proxy-stats:
kubectl -n default exec deploy/kds-proxy -- wget -qO- http://127.0.0.1:3128/metrics

request-fifo-ticket timeout="":
#!/usr/bin/env bash
set -euo pipefail
Expand Down
5 changes: 5 additions & 0 deletions kds-proxy/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/edgelesssys/contrast/kds-proxy

go 1.25.6

require golang.org/x/sync v0.20.0
2 changes: 2 additions & 0 deletions kds-proxy/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
17 changes: 17 additions & 0 deletions kds-proxy/internal/allowlist/allowlist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2026 Edgeless Systems GmbH
// SPDX-License-Identifier: BUSL-1.1

package allowlist

// Default lists the hosts that kds-proxy will proxy to. Any other host is rejected.
var Default = map[string]struct{}{
"kdsintf.amd.com": {},
"api.trustedservices.intel.com": {},
"rim.attestation.nvidia.com": {},
}

// Allows reports whether host is in the allowlist.
func Allows(host string) bool {
_, ok := Default[host]
return ok
}
Loading
Loading