Skip to content

Commit 96b302d

Browse files
committed
Generate SSL cert for ovn and neutron services running on EDPM nodes
This patch adds generations of the individual SSL certificate for each EDPM node. Those certificates are signed with the cert from the OVN SB DB and each of them have CN field set to `uuid5(hostname)` so that the same uuid can be later set as `system-id` on the EDPM node. This is mandatory to make OVN with RBAC working fine. Generated certificates are stored in secret and mounted in the ansibleee POD which provisions ovn-controller service. From there edpm-ansible role can copy it to the EDPM nodes individually. Related: #OSPRH-1921 Related: #OSPRH-1923 Related: #OSPRH-1924 Related: #OSPRH-1925 Signed-off-by: Slawek Kaplonski <skaplons@redhat.com>
1 parent 873083b commit 96b302d

11 files changed

Lines changed: 257 additions & 17 deletions

api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,22 @@ spec:
151151
OpenstackDataPlaneServiceCert defines the property of a TLS cert issued for
152152
a dataplane service
153153
properties:
154+
commonName:
155+
description: |-
156+
CommonName overrides how the certificate Common Name is derived.
157+
When set to "system-id", the CN is a UUID5 derived from the node's
158+
ctlplane FQDN, matching the OVN chassis system-id convention.
159+
When empty, CN defaults to the short hostname.
160+
enum:
161+
- system-id
162+
type: string
154163
contents:
155164
description: |-
156165
Contents of the certificate
157-
This is a list of strings for properties that are needed in the cert
166+
This is a list of strings for properties that are needed in the cert.
167+
May be empty for client-only certificates that require no SANs.
158168
items:
159169
type: string
160-
minItems: 1
161170
type: array
162171
edpmRoleServiceName:
163172
description: |-
@@ -241,8 +250,6 @@ spec:
241250
pattern: ^[a-zA-Z0-9][a-zA-Z0-9\-_]*[a-zA-Z0-9]$
242251
type: string
243252
type: array
244-
required:
245-
- contents
246253
type: object
247254
description: TLSCerts tls certs to be generated
248255
type: object

api/dataplane/v1beta1/openstackdataplaneservice_types.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ import (
2828
// a dataplane service
2929
type OpenstackDataPlaneServiceCert struct {
3030
// Contents of the certificate
31-
// This is a list of strings for properties that are needed in the cert
32-
// +kubebuilder:validation:Required
33-
// +kubebuilder:validation:MinItems:=1
34-
Contents []string `json:"contents"`
31+
// This is a list of strings for properties that are needed in the cert.
32+
// May be empty for client-only certificates that require no SANs.
33+
// +kubebuilder:validation:Optional
34+
Contents []string `json:"contents,omitempty"`
3535

3636
// Networks to include in SNI for the cert
3737
// +kubebuilder:validation:Optional
@@ -46,6 +46,14 @@ type OpenstackDataPlaneServiceCert struct {
4646
// +kubebuilder:validation:Optional
4747
KeyUsages []certmgrv1.KeyUsage `json:"keyUsages,omitempty" yaml:"keyUsages,omitempty"`
4848

49+
// CommonName overrides how the certificate Common Name is derived.
50+
// When set to "system-id", the CN is a UUID5 derived from the node's
51+
// ctlplane FQDN, matching the OVN chassis system-id convention.
52+
// When empty, CN defaults to the short hostname.
53+
// +kubebuilder:validation:Optional
54+
// +kubebuilder:validation:Enum=system-id
55+
CommonName string `json:"commonName,omitempty"`
56+
4957
// EDPMRoleServiceName is the value of the <role>_service_name variable from
5058
// the edpm-ansible role where this certificate is used. For example if the
5159
// certificate is for edpm_ovn from edpm-ansible, EDPMRoleServiceName must be

bindata/crds/crds.yaml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21361,13 +21361,22 @@ spec:
2136121361
OpenstackDataPlaneServiceCert defines the property of a TLS cert issued for
2136221362
a dataplane service
2136321363
properties:
21364+
commonName:
21365+
description: |-
21366+
CommonName overrides how the certificate Common Name is derived.
21367+
When set to "system-id", the CN is a UUID5 derived from the node's
21368+
ctlplane FQDN, matching the OVN chassis system-id convention.
21369+
When empty, CN defaults to the short hostname.
21370+
enum:
21371+
- system-id
21372+
type: string
2136421373
contents:
2136521374
description: |-
2136621375
Contents of the certificate
21367-
This is a list of strings for properties that are needed in the cert
21376+
This is a list of strings for properties that are needed in the cert.
21377+
May be empty for client-only certificates that require no SANs.
2136821378
items:
2136921379
type: string
21370-
minItems: 1
2137121380
type: array
2137221381
edpmRoleServiceName:
2137321382
description: |-
@@ -21451,8 +21460,6 @@ spec:
2145121460
pattern: ^[a-zA-Z0-9][a-zA-Z0-9\-_]*[a-zA-Z0-9]$
2145221461
type: string
2145321462
type: array
21454-
required:
21455-
- contents
2145621463
type: object
2145721464
description: TLSCerts tls certs to be generated
2145821465
type: object

config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,22 @@ spec:
151151
OpenstackDataPlaneServiceCert defines the property of a TLS cert issued for
152152
a dataplane service
153153
properties:
154+
commonName:
155+
description: |-
156+
CommonName overrides how the certificate Common Name is derived.
157+
When set to "system-id", the CN is a UUID5 derived from the node's
158+
ctlplane FQDN, matching the OVN chassis system-id convention.
159+
When empty, CN defaults to the short hostname.
160+
enum:
161+
- system-id
162+
type: string
154163
contents:
155164
description: |-
156165
Contents of the certificate
157-
This is a list of strings for properties that are needed in the cert
166+
This is a list of strings for properties that are needed in the cert.
167+
May be empty for client-only certificates that require no SANs.
158168
items:
159169
type: string
160-
minItems: 1
161170
type: array
162171
edpmRoleServiceName:
163172
description: |-
@@ -241,8 +250,6 @@ spec:
241250
pattern: ^[a-zA-Z0-9][a-zA-Z0-9\-_]*[a-zA-Z0-9]$
242251
type: string
243252
type: array
244-
required:
245-
- contents
246253
type: object
247254
description: TLSCerts tls certs to be generated
248255
type: object

config/services/dataplane_v1beta1_openstackdataplaneservice_neutron_metadata.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ spec:
2424
- digital signature
2525
- key encipherment
2626
- client auth
27+
rbac:
28+
commonName: system-id
29+
issuer: osp-rootca-issuer-ovn-rbac
30+
keyUsages:
31+
- digital signature
32+
- client auth
2733
caCerts: combined-ca-bundle
2834
containerImageFields:
2935
- EdpmNeutronMetadataAgentImage

config/services/dataplane_v1beta1_openstackdataplaneservice_neutron_ovn.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ spec:
2525
- digital signature
2626
- key encipherment
2727
- client auth
28+
rbac:
29+
commonName: system-id
30+
issuer: osp-rootca-issuer-ovn-rbac
31+
keyUsages:
32+
- digital signature
33+
- client auth
2834
caCerts: combined-ca-bundle
2935
containerImageFields:
3036
- EdpmNeutronOvnAgentImage

config/services/dataplane_v1beta1_openstackdataplaneservice_ovn.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ spec:
2020
- key encipherment
2121
- server auth
2222
- client auth
23+
rbac:
24+
commonName: system-id
25+
issuer: osp-rootca-issuer-ovn-rbac
26+
keyUsages:
27+
- digital signature
28+
- client auth
2329
caCerts: combined-ca-bundle
2430
containerImageFields:
2531
- OvnControllerImage

config/services/dataplane_v1beta1_openstackdataplaneservice_ovn_bgp_agent.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ spec:
2323
- key encipherment
2424
- server auth
2525
- client auth
26+
rbac:
27+
commonName: system-id
28+
issuer: osp-rootca-issuer-ovn-rbac
29+
keyUsages:
30+
- digital signature
31+
- client auth
2632
caCerts: combined-ca-bundle
2733
containerImageFields:
2834
- EdpmOvnBgpAgentImage

internal/dataplane/cert.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3636

3737
certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
38+
"github.com/google/uuid"
3839
infranetworkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1"
3940
"github.com/openstack-k8s-operators/lib-common/modules/certmanager"
4041
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
@@ -43,6 +44,17 @@ import (
4344
dataplanev1 "github.com/openstack-k8s-operators/openstack-operator/api/dataplane/v1beta1"
4445
)
4546

47+
// CommonNameSystemID is the sentinel value for OpenstackDataPlaneServiceCert.CommonName
48+
// that triggers UUID5-based CN derivation matching the OVN chassis system-id convention.
49+
const CommonNameSystemID = "system-id"
50+
51+
// computeSystemID derives a deterministic UUID5 from a name using the DNS
52+
// namespace, matching ovn-operator's ComputeSystemID() and edpm-ansible's
53+
// {{ name | to_uuid(namespace='6ba7b810-...') }}.
54+
func computeSystemID(name string) string {
55+
return uuid.NewSHA1(uuid.NameSpaceDNS, []byte(name)).String()
56+
}
57+
4658
// Generates an organized data structure that is leveraged to create the secrets.
4759
func createSecretsDataStructure(secretMaxSize int,
4860
certsData map[string][]byte,
@@ -180,7 +192,12 @@ func EnsureTLSCerts(ctx context.Context, helper *helper.Helper,
180192
nodeName)
181193
}
182194

183-
commonName := strings.Split(baseName, ".")[0]
195+
var commonName string
196+
if service.Spec.TLSCerts[certKey].CommonName == CommonNameSystemID {
197+
commonName = computeSystemID(baseName)
198+
} else {
199+
commonName = strings.Split(baseName, ".")[0]
200+
}
184201

185202
certSecret, result, err = GetTLSNodeCert(ctx, helper, instance, certName,
186203
issuer, labels, commonName, hosts, ips, service.Spec.TLSCerts[certKey].KeyUsages)

internal/dataplane/cert_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package deployment
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestComputeSystemID(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
input string
13+
expected string
14+
}{
15+
{
16+
name: "short hostname",
17+
input: "edpm-compute-0",
18+
expected: computeSystemID("edpm-compute-0"),
19+
},
20+
{
21+
name: "FQDN",
22+
input: "edpm-compute-0.ctlplane.example.com",
23+
expected: computeSystemID("edpm-compute-0.ctlplane.example.com"),
24+
},
25+
{
26+
name: "deterministic: same input always yields same output",
27+
input: "edpm-compute-0",
28+
},
29+
{
30+
name: "different inputs yield different outputs",
31+
input: "edpm-compute-1",
32+
},
33+
}
34+
35+
for _, tt := range tests {
36+
t.Run(tt.name, func(t *testing.T) {
37+
result := computeSystemID(tt.input)
38+
39+
// Must be non-empty
40+
assert.NotEmpty(t, result)
41+
42+
// Must be deterministic
43+
assert.Equal(t, result, computeSystemID(tt.input),
44+
"computeSystemID must be deterministic")
45+
46+
if tt.expected != "" {
47+
assert.Equal(t, tt.expected, result)
48+
}
49+
})
50+
}
51+
52+
// Different inputs must produce different UUIDs
53+
id0 := computeSystemID("edpm-compute-0")
54+
id1 := computeSystemID("edpm-compute-1")
55+
assert.NotEqual(t, id0, id1,
56+
"different hostnames must produce different system IDs")
57+
58+
// Verify format is a valid UUID (8-4-4-4-12 hex)
59+
assert.Regexp(t, `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`,
60+
computeSystemID("test-node"),
61+
"computeSystemID must return a valid UUID string")
62+
}
63+
64+
func TestCreateSecretsDataStructure(t *testing.T) {
65+
tests := []struct {
66+
name string
67+
secretMaxSize int
68+
certsData map[string][]byte
69+
expectedChunks int
70+
}{
71+
{
72+
name: "single node fits in one secret",
73+
secretMaxSize: 1048576,
74+
certsData: map[string][]byte{
75+
"node1-ca.crt": []byte("ca-cert-data"),
76+
"node1-tls.crt": []byte("tls-cert-data"),
77+
"node1-tls.key": []byte("tls-key-data"),
78+
},
79+
expectedChunks: 1,
80+
},
81+
{
82+
name: "small max size forces multiple secrets",
83+
secretMaxSize: 1,
84+
certsData: map[string][]byte{
85+
"node1-ca.crt": []byte("ca-cert-data"),
86+
"node1-tls.crt": []byte("tls-cert-data"),
87+
"node1-tls.key": []byte("tls-key-data"),
88+
"node2-ca.crt": []byte("ca-cert-data"),
89+
"node2-tls.crt": []byte("tls-cert-data"),
90+
"node2-tls.key": []byte("tls-key-data"),
91+
},
92+
expectedChunks: 2,
93+
},
94+
}
95+
96+
for _, tt := range tests {
97+
t.Run(tt.name, func(t *testing.T) {
98+
result := createSecretsDataStructure(tt.secretMaxSize, tt.certsData)
99+
assert.Equal(t, tt.expectedChunks, len(result))
100+
101+
// Verify all data is present across chunks
102+
totalKeys := 0
103+
for _, chunk := range result {
104+
totalKeys += len(chunk)
105+
}
106+
assert.Equal(t, len(tt.certsData), totalKeys,
107+
"all cert data must be present across chunks")
108+
})
109+
}
110+
}

0 commit comments

Comments
 (0)