Skip to content

Commit 7285e1e

Browse files
committed
Generate signed SSL Cert for ovn-controller node
With this patch instead of using the same SSL certificate by each of the ovn-controller PODs in the environment, there is separate certificate generated, with uniq CN name which match system-id set in that chassis and signed with certificate from SB. That way OVN RBAC can be used for the connections from ovn-controller PODs to the OVS SB database. Related: #1922 Assisted-by: claude-opus-4.6 Signed-off-by: Slawek Kaplonski <skaplons@redhat.com>
1 parent ace502d commit 7285e1e

7 files changed

Lines changed: 146 additions & 9 deletions

File tree

internal/ovncontroller/configjob.go

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919

2020
"github.com/openstack-k8s-operators/lib-common/modules/common/env"
2121
ovnv1 "github.com/openstack-k8s-operators/ovn-operator/api/v1beta1"
22+
ovn_common "github.com/openstack-k8s-operators/ovn-operator/internal/common"
23+
ovndbcluster "github.com/openstack-k8s-operators/ovn-operator/internal/ovndbcluster"
2224
"sigs.k8s.io/controller-runtime/pkg/client"
2325

2426
batchv1 "k8s.io/api/batch/v1"
@@ -67,6 +69,38 @@ func ConfigJob(
6769
envVars["OVNLogLevel"] = env.SetValue(instance.Spec.OVNLogLevel)
6870
envVars["OVSLogLevel"] = env.SetValue(instance.Spec.OVSLogLevel)
6971

72+
// Prepare volumes and mounts for config job
73+
volumes := GetOVNControllerVolumes(instance.Name, instance.Namespace, true)
74+
volumeMounts := GetOVNControllerVolumeMounts(true)
75+
76+
// When TLS is enabled, mount the RBAC PKI CA secret so the config job
77+
// can generate and sign per-node ovn-controller certificates.
78+
if instance.Spec.TLS.Enabled() {
79+
volumes = append(volumes, corev1.Volume{
80+
Name: "ovn-rbac-pki-ca",
81+
VolumeSource: corev1.VolumeSource{
82+
Secret: &corev1.SecretVolumeSource{
83+
SecretName: ovndbcluster.OVNRbacPkiCaSecret,
84+
},
85+
},
86+
})
87+
volumeMounts = append(volumeMounts, corev1.VolumeMount{
88+
Name: "ovn-rbac-pki-ca",
89+
MountPath: ovn_common.OVNRbacPkiCaMountPath,
90+
ReadOnly: true,
91+
})
92+
// Also mount etc-ovs to persist the generated certificates
93+
volumeMounts = append(volumeMounts, corev1.VolumeMount{
94+
Name: "etc-ovs",
95+
MountPath: OVNControllerCertDir,
96+
ReadOnly: false,
97+
})
98+
99+
envVars["OVNControllerCertDir"] = env.SetValue(OVNControllerCertDir)
100+
envVars["OVN_RBAC_CA_CERT"] = env.SetValue(ovn_common.OVNRbacPkiCaCertPath)
101+
envVars["OVN_RBAC_CA_KEY"] = env.SetValue(ovn_common.OVNRbacPkiCaKeyPath)
102+
}
103+
70104
for _, ovnPod := range ovnPods.Items {
71105
commands := []string{
72106
"/usr/local/bin/container-scripts/init.sh",
@@ -101,11 +135,11 @@ func ConfigJob(
101135
Privileged: &privileged,
102136
},
103137
Env: env.MergeEnvs([]corev1.EnvVar{}, envVars),
104-
VolumeMounts: GetOVNControllerVolumeMounts(true),
138+
VolumeMounts: volumeMounts,
105139
Resources: instance.Spec.Resources,
106140
},
107141
},
108-
Volumes: GetOVNControllerVolumes(instance.Name, instance.Namespace, true),
142+
Volumes: volumes,
109143
NodeName: ovnPod.Spec.NodeName,
110144
// ^ NodeSelector not required
111145
},

internal/ovncontroller/const.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,11 @@
11
package ovncontroller
2+
3+
const (
4+
// OVNControllerCertDir is the directory where per-node ovn-controller
5+
// RBAC certificates are stored (HostPath-backed)
6+
OVNControllerCertDir string = "/etc/openvswitch"
7+
// OVNControllerCertPath is the path to the per-node ovn-controller certificate
8+
OVNControllerCertPath string = OVNControllerCertDir + "/ovn-controller-cert.pem"
9+
// OVNControllerKeyPath is the path to the per-node ovn-controller private key
10+
OVNControllerKeyPath string = OVNControllerCertDir + "/ovn-controller-privkey.pem"
11+
)

internal/ovncontroller/daemonset.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,19 @@ func CreateOVNDaemonSet(
5959
mounts = append(mounts, instance.Spec.TLS.CreateVolumeMounts(nil)...)
6060
}
6161

62+
// When TLS is enabled, RBAC is used for the SB database connection.
63+
// ovn-controller must use a per-node certificate (CN=node_name) generated
64+
// by the config job and stored in /etc/openvswitch/ (HostPath).
65+
// The CA cert for verifying the SB server remains the same.
66+
mounts = append(mounts, corev1.VolumeMount{
67+
Name: "etc-ovs",
68+
MountPath: OVNControllerCertDir,
69+
ReadOnly: false,
70+
})
71+
6272
cmd = append(cmd, []string{
63-
fmt.Sprintf("--certificate=%s", ovn_common.OVNDbCertPath),
64-
fmt.Sprintf("--private-key=%s", ovn_common.OVNDbKeyPath),
73+
fmt.Sprintf("--certificate=%s", OVNControllerCertPath),
74+
fmt.Sprintf("--private-key=%s", OVNControllerKeyPath),
6575
fmt.Sprintf("--ca-cert=%s", ovn_common.OVNDbCaCertPath),
6676
}...)
6777
}
@@ -239,6 +249,10 @@ func CreateOVSDaemonSet(
239249
envVars := map[string]env.Setter{}
240250
envVars["CONFIG_HASH"] = env.SetValue(configHash)
241251

252+
initEnvVars := map[string]env.Setter{}
253+
initEnvVars["CONFIG_HASH"] = env.SetValue(configHash)
254+
initEnvVars["OVNHostName"] = env.DownwardAPI("spec.nodeName")
255+
242256
initContainers := []corev1.Container{
243257
{
244258
Name: "ovsdb-server-init",
@@ -252,7 +266,7 @@ func CreateOVSDaemonSet(
252266
RunAsUser: &runAsUser,
253267
Privileged: &privileged,
254268
},
255-
Env: env.MergeEnvs([]corev1.EnvVar{}, envVars),
269+
Env: env.MergeEnvs([]corev1.EnvVar{}, initEnvVars),
256270
VolumeMounts: GetOVSDbVolumeMounts(),
257271
},
258272
}

templates/ovncontroller/bin/functions

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ OVNAvailabilityZones=${OVNAvailabilityZones:-""}
2222
EnableChassisAsGateway=${EnableChassisAsGateway:-true}
2323
PhysicalNetworks=${PhysicalNetworks:-""}
2424
OVNHostName=${OVNHostName:-""}
25+
OVNControllerCertDir=${OVNControllerCertDir:-"/etc/openvswitch"}
2526
DB_FILE=/etc/openvswitch/conf.db
2627

2728
ovs_dir=/var/lib/openvswitch
@@ -152,3 +153,63 @@ function wait_for_db_creation {
152153
sleep 1
153154
done
154155
}
156+
157+
# Generate per-node RBAC certificate signed by the OVN RBAC PKI CA.
158+
# The certificate CN is set to the node name (OVNHostName) which must match
159+
# the OVN chassis system-id for RBAC ownership checks to work.
160+
# Certificates are stored in /etc/openvswitch/ which is persisted via HostPath.
161+
function generate_rbac_certificate {
162+
local cert_dir="${OVNControllerCertDir}"
163+
local cert_file="${cert_dir}/ovn-controller-cert.pem"
164+
local key_file="${cert_dir}/ovn-controller-privkey.pem"
165+
local ca_cert="${OVN_RBAC_CA_CERT}"
166+
local ca_key="${OVN_RBAC_CA_KEY}"
167+
168+
if [ -z "${OVNHostName}" ]; then
169+
echo "ERROR: OVNHostName is not set, cannot generate RBAC certificate"
170+
return 1
171+
fi
172+
173+
# Check if CA cert and key are available
174+
if [ ! -f "${ca_cert}" ] || [ ! -f "${ca_key}" ]; then
175+
echo "OVN RBAC PKI CA not available, skipping certificate generation"
176+
return 0
177+
fi
178+
179+
# If certificate already exists and has the correct CN, skip regeneration
180+
if [ -f "${cert_file}" ]; then
181+
existing_cn=$(openssl x509 -in "${cert_file}" -noout -subject 2>/dev/null | sed -n 's/.*CN *= *\([^ ,]*\).*/\1/p')
182+
if [ "${existing_cn}" = "${OVNHostName}" ]; then
183+
echo "RBAC certificate for ${OVNHostName} already exists, skipping generation"
184+
return 0
185+
fi
186+
echo "RBAC certificate CN mismatch (${existing_cn} != ${OVNHostName}), regenerating"
187+
fi
188+
189+
echo "Generating RBAC certificate for chassis ${OVNHostName}"
190+
191+
# Generate private key
192+
openssl genrsa -out "${key_file}" 2048
193+
194+
# Generate CSR with CN=node_name
195+
openssl req -new -key "${key_file}" \
196+
-out "${cert_dir}/ovn-controller.csr" \
197+
-subj "/CN=${OVNHostName}"
198+
199+
# Sign the certificate with the CA.
200+
# Use -CAserial with a writable path since the CA secret mount is read-only.
201+
openssl x509 -req \
202+
-in "${cert_dir}/ovn-controller.csr" \
203+
-CA "${ca_cert}" \
204+
-CAkey "${ca_key}" \
205+
-CAserial "${cert_dir}/ovn-rbac-ca.srl" \
206+
-CAcreateserial \
207+
-out "${cert_file}" \
208+
-days 3650 \
209+
-sha256
210+
211+
# Clean up CSR and serial file
212+
rm -f "${cert_dir}/ovn-controller.csr" "${cert_dir}/ovn-rbac-ca.srl"
213+
214+
echo "RBAC certificate generated successfully for ${OVNHostName}"
215+
}

templates/ovncontroller/bin/init-ovsdb-server.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,14 @@ if [ -f ${DB_FILE} ]; then
2929
ovsdb-tool compact ${DB_FILE}
3030
fi
3131

32-
# Initialize or upgrade database if needed
33-
CTL_ARGS="--system-id=random --no-ovs-vswitchd"
32+
# Initialize or upgrade database if needed.
33+
# Use deterministic system-id based on node name for OVN RBAC compatibility.
34+
# The system-id must match the certificate CN for RBAC ownership checks.
35+
if [ -n "${OVNHostName}" ]; then
36+
CTL_ARGS="--system-id=${OVNHostName} --no-ovs-vswitchd"
37+
else
38+
CTL_ARGS="--system-id=random --no-ovs-vswitchd"
39+
fi
3440
/usr/share/openvswitch/scripts/ovs-ctl start $CTL_ARGS
3541
/usr/share/openvswitch/scripts/ovs-ctl stop $CTL_ARGS
3642

templates/ovncontroller/bin/init.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,16 @@ wait_for_ovsdb_server
2121
# From now on, we should exit immediatelly when any command exits with non-zero status
2222
set -ex
2323

24+
# Set deterministic system-id based on node name for RBAC compatibility.
25+
# The system-id must match the certificate CN for OVN RBAC ownership checks.
26+
if [ -n "${OVNHostName}" ]; then
27+
ovs-vsctl set open . external-ids:system-id=${OVNHostName}
28+
fi
29+
2430
configure_external_ids
2531
configure_physical_networks
32+
33+
# Generate per-node RBAC certificate if the RBAC PKI CA is available
34+
if [ -n "${OVN_RBAC_CA_CERT}" ] && [ -f "${OVN_RBAC_CA_CERT}" ]; then
35+
generate_rbac_certificate
36+
fi

test/functional/ovncontroller_controller_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
3232
ovnv1 "github.com/openstack-k8s-operators/ovn-operator/api/v1beta1"
3333
ovn_common "github.com/openstack-k8s-operators/ovn-operator/internal/common"
34+
ovn_ovncontroller "github.com/openstack-k8s-operators/ovn-operator/internal/ovncontroller"
3435
appsv1 "k8s.io/api/apps/v1"
3536
batchv1 "k8s.io/api/batch/v1"
3637
corev1 "k8s.io/api/core/v1"
@@ -1048,8 +1049,8 @@ var _ = Describe("OVNController controller", func() {
10481049

10491050
// check cli args
10501051
Expect(svcC.Command).To(And(
1051-
ContainElement(ContainSubstring(fmt.Sprintf("--private-key=%s", ovn_common.OVNDbKeyPath))),
1052-
ContainElement(ContainSubstring(fmt.Sprintf("--certificate=%s", ovn_common.OVNDbCertPath))),
1052+
ContainElement(ContainSubstring(fmt.Sprintf("--private-key=%s", ovn_ovncontroller.OVNControllerKeyPath))),
1053+
ContainElement(ContainSubstring(fmt.Sprintf("--certificate=%s", ovn_ovncontroller.OVNControllerCertPath))),
10531054
ContainElement(ContainSubstring(fmt.Sprintf("--ca-cert=%s", ovn_common.OVNDbCaCertPath))),
10541055
))
10551056

0 commit comments

Comments
 (0)