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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ The setup of the Service Provider Landscaper within an OpenControlPlane landsca
| Status reporting & error messages | ✅ | |
| Operation annotations | ⚠️ | `landscaper.services.openmcp.cloud/operation: reconcile` is processed. `openmcp.cloud/operation: ignore` is not processed. |
| API stability policy | ✅ | |
| Custom CA support | | Custom CA bundle propagation to Landscaper components is not implemented. |
| Custom CA support | | |
| Release artifacts (image + OCM) | ✅ | |
| Testing | ⚠️ | Unit and envtest suites exist; full lifecycle tests against a real cluster are not present. |
| Ownership and maintenance docs | ✅ | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,31 @@ spec:
description: ProviderConfigSpec is the specification of the Landscaper
Service Provider configuration
properties:
caBundleRef:
description: |-
CABundleRef is a reference to a config map containing a PEM-encoded certificate bundle.
It will be installed on the OpenControlPlane and configured for the domain service.
properties:
key:
description: The key to select.
type: string
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
optional:
description: Specify whether the ConfigMap or its key must be
defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
deployment:
description: Deployment specifies the OCI image locations and available
versions of the landscaper
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha2/providerconfiguration_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package v1alpha2

import (
"github.com/openmcp-project/openmcp-operator/api/common"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
Expand All @@ -34,6 +35,10 @@ const (
type ProviderConfigSpec struct {
// +kubebuilder:validation:Required
Deployment Deployment `json:"deployment"`
// CABundleRef is a reference to a config map containing a PEM-encoded certificate bundle.
// It will be installed on the OpenControlPlane and configured for the domain service.
// +kubebuilder:validation:Optional
CABundleRef *corev1.ConfigMapKeySelector `json:"caBundleRef,omitempty"`
}

// ProviderConfigStatus is the status of the Landscaper Service Provider configuration
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

## Technical Documentation

- [Custom CA Bundle Configuration](technical/custom-ca.md)
- [Installing the Service Provider Landscaper](technical/install.md)
- [Packages](technical/packages.md)

20 changes: 20 additions & 0 deletions docs/resources/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,26 @@ spec:
image: my.registry.example/custom-manifest-deployer
```

### Custom CA Certificates

The `CABundleRef` property in the `ProviderConfig` allows you to configure custom Certificate Authority (CA) bundles for the Landscaper instances as shown below.
For a more detailed guide see the [custom CA guide](../technical/custom-ca.md).

```yaml
apiVersion: landscaper.services.openmcp.cloud/v1alpha2
kind: ProviderConfig
metadata:
name: default
labels:
landscaper.services.openmcp.cloud/providertype: default
spec:
# ... other configuration ...

caBundleRef:
name: custom-ca-bundle # Name of the ConfigMap
key: ca-bundle.crt # Key within the ConfigMap containing the certificate bundle
```

### Default ProviderConfig

If the label `landscaper.services.openmcp.cloud/providertype: default` is set, this `ProviderConfig` is used by all `Landscaper` resources that do not explicitly reference a provider configuration.
Expand Down
115 changes: 115 additions & 0 deletions docs/technical/custom-ca.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Custom CA Bundle Configuration

## Overview

The `CABundleRef` property in the `ProviderConfig` allows you to configure custom Certificate Authority (CA) bundles for Landscaper instances. This is essential when your infrastructure uses private or self-signed certificates that need to be trusted by Landscaper.

## Use Cases

- **Private Source Registries**: When pulling Helm charts from registries with self-signed certificates
- **Air-gapped Environments**: When operating in isolated networks with custom certificate infrastructure

## Configuration

### ProviderConfig Specification

The `caBundleRef` field is an optional reference to a Kubernetes ConfigMap containing your custom CA certificate bundle:

```yaml
apiVersion: landscaper.services.openmcp.cloud/v1alpha2
kind: ProviderConfig
metadata:
name: default
labels:
landscaper.services.openmcp.cloud/providertype: default
spec:
# ... other configuration ...

caBundleRef:
name: custom-ca-bundle # Name of the ConfigMap
key: ca-bundle.crt # Key within the ConfigMap containing the certificate bundle
```

### Creating the CA Bundle ConfigMap

First, create a ConfigMap in the Platform cluster containing your CA certificate bundle. The certificate bundle should be in PEM format and can contain multiple certificates concatenated together.

#### Example: Single CA Certificate

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: custom-ca-bundle
namespace: openmcp-system # Must be in the same namespace as the service-provider-landscaper
data:
ca-bundle.crt: |
-----BEGIN CERTIFICATE-----
MIIDADCCAeigAwIBAgIUU0jjGMPVbvVbUen942ixQO2k2V4wDQYJKoZIhvcNAQEL
BQAwGDEWMBQGA1UEAxMNc2VsZnNpZ25lZC1jYTAeFw0yNjAyMDkyMDQ5MTdaFw0y
... (certificate content) ...
-----END CERTIFICATE-----
```

#### Example: Multiple CA Certificates

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: custom-ca-bundle
namespace: openmcp-system
data:
ca-bundle.crt: |
-----BEGIN CERTIFICATE-----
MIIDADCCAeigAwIBAgIUU0jjGMPVbvVbUen942ixQO2k2V4wDQYJKoZIhvcNAQEL
... (first certificate) ...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDADCCAeigAwIBAgIUU0jjGMPVbvVbUen942ixQO2k2V4wDQYJKoZIhvcNAQEL
... (second certificate) ...
-----END CERTIFICATE-----
```

#### Creating from a Certificate File

If you have CA certificates in files, you can create the ConfigMap directly:

```bash
kubectl create configmap custom-ca-bundle \
--from-file=ca-bundle.crt=/path/to/your/ca-bundle.crt \
--namespace=openmcp-system
```

## How It Works

```mermaid
flowchart TB

subgraph PC[Platform Cluster]
subgraph SPN[Service Provider Namespace]
customcacm([custom-ca-configmap])
end
end

subgraph MCP[WorkloadCluster]
subgraph FS[landscaper namespace]
customcacmcopy([custom-ca-configmap])
HelmDeployer -. uses .-> customcacmcopy
ManifestDeployer -. uses .-> customcacmcopy
ControllerDeployment -. uses .-> customcacmcopy
MainDeployment -. uses .-> customcacmcopy
WebhooksServerDeployment -. uses .-> customcacmcopy
end
end

customcacm -- copied to --> customcacmcopy
```

When you configure `spec.caBundleRef` in a ProviderConfig, the service provider reads the referenced ConfigMap (name/key) from the provider namespace on the platform cluster and copies it into the Landscaper instance namespace on the workload cluster.

Each Landscaper component (controller, main, webhooks, helm deployer, manifest deployer) mounts the copied ConfigMap as a volume named `custom-ca-bundle` at `/etc/open-control-plane/custom-ca`. Only the selected ConfigMap key is mounted as a file with the key as filename `/etc/open-control-plane/custom-ca/<configmap-key>`.

To make the bundle effective, the pods set `SSL_CERT_DIR=<default-cert-dirs>:/etc/open-control-plane/custom-ca`. This keeps the system trust store and extends it with your custom CA bundle.

**Notice:** If your container runtime also needs to trust a private registry, install the CA bundle on the cluster nodes as well.
57 changes: 57 additions & 0 deletions internal/controller/landscaper_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ func (r *LandscaperReconciler) SetupWithManager(mgr ctrl.Manager) error {
WatchesRawSource(source.Kind(r.PlatformCluster.Cluster().GetCache(), &corev1.Secret{},
handler.TypedEnqueueRequestsFromMapFunc(r.mapImagePullSecretToRequests(mgr)),
)).
WatchesRawSource(source.Kind(r.PlatformCluster.Cluster().GetCache(), &corev1.ConfigMap{},
handler.TypedEnqueueRequestsFromMapFunc(r.mapCABundleConfigMapToRequests(mgr)),
)).
Named(controllerName).
Complete(r)
}
Expand Down Expand Up @@ -247,6 +250,60 @@ func referencesSecret(refs []common.LocalObjectReference, name string) bool {
return false
}

// mapCABundleConfigMapToRequests returns a handler function that triggers reconciliation of Landscaper resources
// whenever a ProviderConfig-referenced CA bundle ConfigMap changes.
func (r *LandscaperReconciler) mapCABundleConfigMapToRequests(mgr ctrl.Manager) func(context.Context, *corev1.ConfigMap) []ctrl.Request {
return func(ctx context.Context, configMap *corev1.ConfigMap) []ctrl.Request {
log := logging.Wrap(mgr.GetLogger()).WithName(controllerName + "/ConfigMap")

if configMap.Namespace != r.ProviderNamespace {
return nil
}

if !r.isReferencedCaConfigMap(ctx, configMap.Name) {
return nil
}

log.Debug("CA bundle configmap changed, triggering reconcile", "configMap", configMap.Name)

landscapers := &v1alpha2.LandscaperList{}
if err := r.OnboardingCluster.Client().List(ctx, landscapers); err != nil {
log.Error(err, "Failed to list Landscaper resources")
return nil
}

for _, landscaper := range landscapers.Items {
if err := controller.EnsureAnnotation(
ctx, r.OnboardingCluster.Client(),
&landscaper,
v1alpha2.LandscaperOperation, v1alpha2.OperationReconcile,
true, controller.OVERWRITE); err != nil {
log.Error(err, "Failed to set reconcile annotation for Landscaper resource", "landscaper", landscaper.Name, "namespace", landscaper.Namespace)
}
}
return nil
}
}

// isReferencedCaConfigMap checks whether the given configmap name is referenced as a CA bundle
// in any ProviderConfig resource on the platform cluster.
func (r *LandscaperReconciler) isReferencedCaConfigMap(ctx context.Context, configMapName string) bool {
log := logging.Wrap(ctrl.Log).WithName(controllerName + "/ConfigMap")

providerConfigList := &v1alpha2.ProviderConfigList{}
if err := r.PlatformCluster.Client().List(ctx, providerConfigList); err != nil {
log.Error(err, "Failed to list ProviderConfig resources")
return false
}

for _, providerConfig := range providerConfigList.Items {
if providerConfig.Spec.CABundleRef != nil && providerConfig.Spec.CABundleRef.Name == configMapName {
return true
}
}
return false
}

func getMCPPermissions() []clustersv1alpha1.PermissionsRequest {
defaultVerbs := []string{"get", "list", "watch", "create", "update", "patch", "delete"}

Expand Down
58 changes: 57 additions & 1 deletion internal/controller/landscaper_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"k8s.io/utils/ptr"

"github.com/openmcp-project/service-provider-landscaper/internal/dns"
configmapsync "github.com/openmcp-project/service-provider-landscaper/internal/shared/configmaps"

"github.com/openmcp-project/service-provider-landscaper/internal/shared/identity"

Expand Down Expand Up @@ -42,7 +43,9 @@ import (
)

const (
controllerName = "test-controller"
controllerName = "test-controller"
caConfigMapName = "ca-bundle"
caConfigMapKey = "ca.crt"
)

func setDeploymentReady(ctx context.Context, deployment *appsv1.Deployment, c client.Client) {
Expand Down Expand Up @@ -79,6 +82,52 @@ func expectImagePullSecretsValid(ctx context.Context, c client.Client, deploymen
}
}

// expectCaConfigMapValid verifies that a deployment mounts CA configmaps at the expected path
// and that each referenced configmap exists in the given namespace with the expected key.
func expectCaConfigMapValid(ctx context.Context, c client.Client, deployment *appsv1.Deployment, namespace string) {
var foundVolumeMount bool
var foundVolume bool
var foundEnvVar bool

for _, container := range deployment.Spec.Template.Spec.Containers {
for _, volumeMount := range container.VolumeMounts {
if volumeMount.Name == configmapsync.CustomCaVolumeName {
foundVolumeMount = true
break
}
}
}

for _, volume := range deployment.Spec.Template.Spec.Volumes {
if volume.Name == configmapsync.CustomCaVolumeName {
foundVolume = true
break
}
}

for _, container := range deployment.Spec.Template.Spec.Containers {
for _, envVar := range container.Env {
if envVar.Value == configmapsync.SSLCertDirEnvValue() {
foundEnvVar = true
}
}
}

Expect(foundVolumeMount).To(BeTrue())
Expect(foundVolume).To(BeTrue())
Expect(foundEnvVar).To(BeTrue())

configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: caConfigMapName,
Namespace: namespace,
},
}
Expect(c.Get(ctx, client.ObjectKeyFromObject(configMap), configMap)).To(Succeed())
Expect(configMap.Data).To(HaveKey(caConfigMapKey))

}

type testInstanceClusterAccess struct {
mcpCluster *clusters.Cluster
workloadCluster *clusters.Cluster
Expand Down Expand Up @@ -396,6 +445,13 @@ var _ = Describe("Landscaper Controller", func() {
expectImagePullSecretsValid(env.Ctx, env.Client(), manifestDeployerDeployment, 2, installationNamespace)
expectImagePullSecretsValid(env.Ctx, env.Client(), helmDeployerDeployment, 1, installationNamespace)

// expect the synced CA configmap to be mounted for all component deployments
expectCaConfigMapValid(env.Ctx, env.Client(), lsControllerDeployment, installationNamespace)
expectCaConfigMapValid(env.Ctx, env.Client(), lsMainDeployment, installationNamespace)
expectCaConfigMapValid(env.Ctx, env.Client(), lsWebhooksServerDeployment, installationNamespace)
expectCaConfigMapValid(env.Ctx, env.Client(), manifestDeployerDeployment, installationNamespace)
expectCaConfigMapValid(env.Ctx, env.Client(), helmDeployerDeployment, installationNamespace)

// set deployments to ready
setDeploymentReady(env.Ctx, lsControllerDeployment, env.Client())
setDeploymentReady(env.Ctx, lsMainDeployment, env.Client())
Expand Down
Loading
Loading