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
Original file line number Diff line number Diff line change
Expand Up @@ -1722,6 +1722,13 @@ func (r *HostedControlPlaneReconciler) reconcilePKI(ctx context.Context, hcp *hy
}
}
case hyperv1.AzurePlatform:
azureWorkloadIdentityWebhookServingCert := manifests.AzureWorkloadIdentityWebhookServingCert(hcp.Namespace)
if _, err := createOrUpdate(ctx, r, azureWorkloadIdentityWebhookServingCert, func() error {
return pki.ReconcileAzureWorkloadIdentityWebhookServingCert(azureWorkloadIdentityWebhookServingCert, rootCASecret, p.OwnerRef)
}); err != nil {
return fmt.Errorf("failed to reconcile %s secret: %w", azureWorkloadIdentityWebhookServingCert.Name, err)
}

azureDiskCsiDriverControllerMetricsService := manifests.AzureDiskCsiDriverControllerMetricsService(hcp.Namespace)
if err = r.Get(ctx, client.ObjectKeyFromObject(azureDiskCsiDriverControllerMetricsService), azureDiskCsiDriverControllerMetricsService); err != nil {
if !apierrors.IsNotFound(err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,12 @@ func AzureFileConfigWithCredentials(ns string) *corev1.Secret {
},
}
}

func AzureWorkloadIdentityWebhookKubeconfig(ns string) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "azure-workload-identity-webhook-kubeconfig",
Namespace: ns,
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@ func AWSPodIdentityWebhookServingCert(ns string) *corev1.Secret {
return secretFor(ns, "aws-pod-identity-webhook-serving-cert")
}

func AzureWorkloadIdentityWebhookServingCert(ns string) *corev1.Secret {
return secretFor(ns, "azure-workload-identity-webhook-serving-cert")
}

func AzureDiskCsiDriverControllerMetricsServingCert(ns string) *corev1.Secret {
return secretFor(ns, "azure-disk-csi-driver-controller-metrics-serving-cert")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package pki

import (
"github.com/openshift/hypershift/support/config"

corev1 "k8s.io/api/core/v1"
)

func ReconcileAzureWorkloadIdentityWebhookServingCert(secret, ca *corev1.Secret, ownerRef config.OwnerRef) error {
return reconcileSignedCertWithAddresses(secret, ca, ownerRef, "127.0.0.1", nil, X509UsageClientServerAuth, nil, []string{"127.0.0.1"})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package pki

import (
"testing"

"github.com/openshift/hypershift/support/certs"
"github.com/openshift/hypershift/support/config"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
)

func TestReconcileAzureWorkloadIdentityWebhookServingCert(t *testing.T) {
testCases := []struct {
name string
}{
{
name: "When reconciling the serving cert it should generate a valid TLS certificate for 127.0.0.1",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
namespace := "test-namespace"

ownerRef := config.OwnerRef{
Reference: &metav1.OwnerReference{
APIVersion: "v1",
Kind: "HostedControlPlane",
Name: "test-hcp",
UID: types.UID("test-uid"),
Controller: ptr.To(true),
},
}

ca := &corev1.Secret{}
ca.Name = "test-ca"
ca.Namespace = namespace
if err := reconcileSelfSignedCA(ca, ownerRef, "test-org", "test-ca"); err != nil {
t.Fatalf("failed to create CA: %v", err)
}

secret := &corev1.Secret{}
secret.Name = "azure-workload-identity-webhook-serving-cert"
secret.Namespace = namespace

if err := ReconcileAzureWorkloadIdentityWebhookServingCert(secret, ca, ownerRef); err != nil {
t.Fatalf("failed to reconcile cert: %v", err)
}

if secret.Data == nil {
t.Fatal("secret data is nil")
}

if _, ok := secret.Data[corev1.TLSCertKey]; !ok {
t.Fatal("secret missing tls.crt")
}

if _, ok := secret.Data[corev1.TLSPrivateKeyKey]; !ok {
t.Fatal("secret missing tls.key")
}

cert, err := certs.PemToCertificate(secret.Data[corev1.TLSCertKey])
if err != nil {
t.Fatalf("failed to parse certificate: %v", err)
}

// The cert should have 127.0.0.1 as an IP SAN
if len(cert.IPAddresses) != 1 || cert.IPAddresses[0].String() != "127.0.0.1" {
t.Errorf("expected IP SAN [127.0.0.1], got %v", cert.IPAddresses)
}

// The CN should be 127.0.0.1
if cert.Subject.CommonName != "127.0.0.1" {
t.Errorf("expected CN 127.0.0.1, got %s", cert.Subject.CommonName)
}

// IP-only certs generated by reconcileSignedCertWithAddresses do not set an organization
if len(cert.Subject.Organization) != 0 {
t.Errorf("expected empty Organization for IP-based cert, got %v", cert.Subject.Organization)
}
})
}
}

func TestReconcileAzureWorkloadIdentityWebhookServingCertIdempotent(t *testing.T) {
t.Run("When reconciling the serving cert twice it should produce the same certificate", func(t *testing.T) {
namespace := "test-namespace"

ownerRef := config.OwnerRef{
Reference: &metav1.OwnerReference{
APIVersion: "v1",
Kind: "HostedControlPlane",
Name: "test-hcp",
UID: types.UID("test-uid"),
Controller: ptr.To(true),
},
}

ca := &corev1.Secret{}
ca.Name = "test-ca"
ca.Namespace = namespace
if err := reconcileSelfSignedCA(ca, ownerRef, "test-org", "test-ca"); err != nil {
t.Fatalf("failed to create CA: %v", err)
}

secret := &corev1.Secret{}
secret.Name = "azure-workload-identity-webhook-serving-cert"
secret.Namespace = namespace

if err := ReconcileAzureWorkloadIdentityWebhookServingCert(secret, ca, ownerRef); err != nil {
t.Fatalf("first reconcile failed: %v", err)
}

firstCert := make([]byte, len(secret.Data[corev1.TLSCertKey]))
copy(firstCert, secret.Data[corev1.TLSCertKey])

if err := ReconcileAzureWorkloadIdentityWebhookServingCert(secret, ca, ownerRef); err != nil {
t.Fatalf("second reconcile failed: %v", err)
}

if string(firstCert) != string(secret.Data[corev1.TLSCertKey]) {
t.Error("expected idempotent reconciliation to produce the same certificate")
}
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,16 @@ func ReconcileHCCOClientCertSecret(secret, ca *corev1.Secret, ownerRef config.Ow
}

func ReconcileServiceAccountKubeconfig(secret, csrSigner *corev1.Secret, ca *corev1.ConfigMap, hcp *hyperv1.HostedControlPlane, serviceAccountNamespace, serviceAccountName string) error {
svcURL := inClusterKASURL(hcp.Spec.Platform.Type)
return ReconcileServiceAccountKubeconfigWithURL(secret, csrSigner, ca, serviceAccountNamespace, serviceAccountName, svcURL)
}

func ReconcileServiceAccountKubeconfigWithURL(secret, csrSigner *corev1.Secret, ca *corev1.ConfigMap, serviceAccountNamespace, serviceAccountName, kubeconfigURL string) error {
cn := serviceaccount.MakeUsername(serviceAccountNamespace, serviceAccountName)
if err := reconcileSignedCert(secret, csrSigner, config.OwnerRef{}, cn, serviceaccount.MakeGroupNames(serviceAccountNamespace), X509UsageClientAuth); err != nil {
return fmt.Errorf("failed to reconcile serviceaccount client cert: %w", err)
}
svcURL := inClusterKASURL(hcp.Spec.Platform.Type)
return ReconcileKubeConfig(secret, secret, ca, svcURL, "", manifests.KubeconfigScopeLocal, config.OwnerRef{})
return ReconcileKubeConfig(secret, secret, ca, kubeconfigURL, "", manifests.KubeconfigScopeLocal, config.OwnerRef{})
}

func ReconcileKubeConfig(secret, cert *corev1.Secret, ca *corev1.ConfigMap, url string, key string, scope manifests.KubeconfigScope, ownerRef config.OwnerRef) error {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
package pki

import "testing"
import (
"crypto/x509/pkix"
"testing"

"github.com/openshift/hypershift/support/certs"
"github.com/openshift/hypershift/support/util"

corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/clientcmd"
)

func TestAddBracketsIfIPv6(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -53,3 +62,59 @@ func TestAddBracketsIfIPv6(t *testing.T) {
})
}
}

func TestReconcileServiceAccountKubeconfigWithURL(t *testing.T) {
t.Parallel()

caCfg := certs.CertCfg{
IsCA: true,
Subject: pkix.Name{CommonName: "root-ca", OrganizationalUnit: []string{"unit"}},
}
caKey, caCert, err := certs.GenerateSelfSignedCertificate(&caCfg)
if err != nil {
t.Fatalf("failed to generate CA: %v", err)
}

csrSigner := &corev1.Secret{
Data: map[string][]byte{
certs.CASignerCertMapKey: certs.CertToPem(caCert),
certs.CASignerKeyMapKey: certs.PrivateKeyToPem(caKey),
},
}
caConfigMap := &corev1.ConfigMap{
Data: map[string]string{
certs.CASignerCertMapKey: string(certs.CertToPem(caCert)),
},
}
secret := &corev1.Secret{}
localhostURL := "https://localhost:9443"

testCases := []struct {
name string
}{
{
name: "When reconciling service account kubeconfig with explicit URL it should use that URL as cluster server",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if err := ReconcileServiceAccountKubeconfigWithURL(secret, csrSigner, caConfigMap, "openshift-authentication", "azure-workload-identity-webhook", localhostURL); err != nil {
t.Fatalf("failed to reconcile kubeconfig: %v", err)
}

kubeconfigData, hasKubeconfig := secret.Data[util.KubeconfigKey]
if !hasKubeconfig {
t.Fatalf("expected %q key to be present in secret data", util.KubeconfigKey)
}

kubeconfig, err := clientcmd.Load(kubeconfigData)
if err != nil {
t.Fatalf("failed to parse kubeconfig data: %v", err)
}
if kubeconfig.Clusters["cluster"].Server != localhostURL {
t.Fatalf("expected kubeconfig server %q, got %q", localhostURL, kubeconfig.Clusters["cluster"].Server)
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apiVersion: v1
data:
ca.crt: ""
kubeconfig: ""
tls.crt: ""
tls.key: ""
kind: Secret
metadata:
annotations:
hypershiftlite.openshift.io/ca-hash: ""
labels:
hypershift.openshift.io/kubeconfig: local
name: azure-workload-identity-webhook-kubeconfig
namespace: hcp-namespace
ownerReferences:
- apiVersion: hypershift.openshift.io/v1beta1
blockOwnerDeletion: true
controller: true
kind: HostedControlPlane
name: hcp
uid: ""
resourceVersion: "1"
type: Opaque
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ status:
- group: secrets-store.csi.x-k8s.io
kind: SecretProviderClass
name: managed-azure-kms
- group: ""
kind: Secret
name: azure-workload-identity-webhook-kubeconfig
- group: ""
kind: Secret
name: bootstrap-kubeconfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ spec:
metadata:
annotations:
cluster-autoscaler.kubernetes.io/safe-to-evict-local-volumes: bootstrap-manifests,logs,kms-socket,tmp-dir
component.hypershift.openshift.io/config-hash: 0fd3eed819dc307e1d8949e2360eec75794dc8cc8b110e88a2a4098ca7ac042de7e69167
component.hypershift.openshift.io/config-hash: 0fd3eed819dc307e1d8949e2360eec75741638a5741638a5741638a5741638a5794dc8cc8b110e88a2a4098ca7ac042de7e69167
hypershift.openshift.io/release-image: quay.io/openshift-release-dev/ocp-release:4.16.10-x86_64
labels:
app: kube-apiserver
Expand Down Expand Up @@ -305,6 +305,65 @@ spec:
name: logs
- mountPath: /tmp
name: tmp-dir
- args:
- |
set -u
until curl -kfsS "https://localhost:6443/version" >/dev/null; do
echo "waiting for kube-apiserver /version endpoint to become available"
sleep 2
done
exec /usr/bin/azure-workload-identity-webhook \
--webhook-cert-dir=/var/run/app/certs \
--health-addr=:9440 \
--audience=api://AzureADTokenExchange \
--kubeconfig=/var/run/app/kubeconfig/kubeconfig \
--metrics-addr=:9441 \
--log-level=info \
--disable-cert-rotation
command:
- /bin/sh
- -ec
env:
- name: AZURE_TENANT_ID
- name: AZURE_ENVIRONMENT
value: AzurePublicCloud
Comment thread
csrwng marked this conversation as resolved.
image: azure-workload-identity-webhook
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
path: /healthz
port: 9440
scheme: HTTP
periodSeconds: 20
name: azure-workload-identity-webhook
readinessProbe:
httpGet:
path: /readyz
port: 9440
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: 10m
memory: 25Mi
securityContext:
readOnlyRootFilesystem: true
startupProbe:
failureThreshold: 30
httpGet:
path: /healthz
port: 9440
scheme: HTTP
periodSeconds: 10
terminationMessagePolicy: FallbackToLogsOnError
volumeMounts:
- mountPath: /var/run/app/certs
name: azure-wi-webhook-serving-certs
- mountPath: /var/run/app/kubeconfig
name: azure-wi-webhook-kubeconfig
- mountPath: /tmp
name: tmp-dir
- args:
- --keyvault-name=test-kms-keyvault
- --key-name=test-key
Expand Down Expand Up @@ -585,6 +644,14 @@ spec:
secret:
defaultMode: 416
secretName: konnectivity-cluster
- name: azure-wi-webhook-serving-certs
secret:
defaultMode: 416
secretName: azure-workload-identity-webhook-serving-cert
- name: azure-wi-webhook-kubeconfig
secret:
defaultMode: 416
secretName: azure-workload-identity-webhook-kubeconfig
- name: kas-secret-encryption-config
secret:
defaultMode: 416
Expand Down
Loading