Skip to content

Commit 6f6a58a

Browse files
committed
pki: add PKI configuration types, validation, and CR manifest generation
Add the configurable PKI API surface to InstallConfig behind the ConfigurablePKI feature gate. When the gate is active, the installer generates a config.openshift.io/v1alpha1 PKI custom resource manifest that day-2 operators use for certificate rotation parameters. The default PKI profile uses RSA-4096 until all day-2 operators (CKAO, CKMO, etc.) support ECDSA rotation. When pki is not specified in install-config the PKI CR uses mode: Default. When pki is specified the PKI CR uses mode: Custom with DefaultPKIProfile as the base and user signerCertificates overrides layered on top. No certificate generation changes are included — all certs remain RSA-2048. Non-TechPreview clusters are completely unaffected. Assisted-by: Claude Code (Opus 4.6)
1 parent 79e3534 commit 6f6a58a

17 files changed

Lines changed: 1200 additions & 1 deletion

data/data/install.openshift.io_installconfigs.yaml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5084,6 +5084,82 @@ spec:
50845084
- rhel-9
50855085
- rhel-10
50865086
type: string
5087+
pki:
5088+
description: |-
5089+
PKI configures cryptographic parameters for installer-generated
5090+
signer certificates. When specified, all signer certificates use the
5091+
algorithm and parameters from signerCertificates.
5092+
Feature gated by ConfigurablePKI.
5093+
properties:
5094+
signerCertificates:
5095+
description: |-
5096+
signerCertificates specifies key parameters for all installer-generated
5097+
certificate authority (CA) certificates.
5098+
When set, all signer certificates use the specified algorithm and parameters.
5099+
minProperties: 1
5100+
properties:
5101+
key:
5102+
description: key specifies the cryptographic parameters for the
5103+
certificate's key pair.
5104+
properties:
5105+
algorithm:
5106+
description: |-
5107+
algorithm specifies the key generation algorithm.
5108+
Valid values are "RSA" and "ECDSA".
5109+
enum:
5110+
- RSA
5111+
- ECDSA
5112+
type: string
5113+
ecdsa:
5114+
description: |-
5115+
ecdsa specifies ECDSA key parameters.
5116+
Required when algorithm is ECDSA, and forbidden otherwise.
5117+
properties:
5118+
curve:
5119+
description: |-
5120+
curve specifies the NIST elliptic curve for ECDSA keys.
5121+
Valid values are "P256", "P384", and "P521".
5122+
enum:
5123+
- P256
5124+
- P384
5125+
- P521
5126+
type: string
5127+
required:
5128+
- curve
5129+
type: object
5130+
rsa:
5131+
description: |-
5132+
rsa specifies RSA key parameters.
5133+
Required when algorithm is RSA, and forbidden otherwise.
5134+
properties:
5135+
keySize:
5136+
description: |-
5137+
keySize specifies the size of RSA keys in bits.
5138+
Valid values are multiples of 1024 from 2048 to 8192.
5139+
format: int32
5140+
maximum: 8192
5141+
minimum: 2048
5142+
multipleOf: 1024
5143+
type: integer
5144+
required:
5145+
- keySize
5146+
type: object
5147+
required:
5148+
- algorithm
5149+
type: object
5150+
x-kubernetes-validations:
5151+
- message: rsa is required when algorithm is RSA, and forbidden
5152+
otherwise
5153+
rule: 'has(self.algorithm) && self.algorithm == ''RSA'' ? has(self.rsa)
5154+
: !has(self.rsa)'
5155+
- message: ecdsa is required when algorithm is ECDSA, and forbidden
5156+
otherwise
5157+
rule: 'has(self.algorithm) && self.algorithm == ''ECDSA'' ? has(self.ecdsa)
5158+
: !has(self.ecdsa)'
5159+
type: object
5160+
required:
5161+
- signerCertificates
5162+
type: object
50875163
platform:
50885164
description: |-
50895165
Platform is the configuration for the specific platform upon which to

pkg/asset/manifests/operators.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ func (m *Manifests) Dependencies() []asset.Asset {
9090
&bootkube.InternalReleaseImageTLSSecret{},
9191
&bootkube.InternalReleaseImageRegistryAuthSecret{},
9292
&BMCVerifyCAConfigMap{},
93+
&PKIConfiguration{},
9394
}
9495
}
9596

@@ -107,8 +108,9 @@ func (m *Manifests) Generate(_ context.Context, dependencies asset.Parents) erro
107108
imageDigestMirrorSet := &ImageDigestMirrorSet{}
108109
mcoCfgTemplate := &manifests.MCO{}
109110
bmcVerifyCAConfigMap := &BMCVerifyCAConfigMap{}
111+
pkiConfig := &PKIConfiguration{}
110112

111-
dependencies.Get(installConfig, ingress, dns, network, infra, proxy, scheduler, imageContentSourcePolicy, imageDigestMirrorSet, clusterCSIDriverConfig, mcoCfgTemplate, bmcVerifyCAConfigMap)
113+
dependencies.Get(installConfig, ingress, dns, network, infra, proxy, scheduler, imageContentSourcePolicy, imageDigestMirrorSet, clusterCSIDriverConfig, mcoCfgTemplate, bmcVerifyCAConfigMap, pkiConfig)
112114

113115
redactedConfig, err := redactedInstallConfig(*installConfig.Config)
114116
if err != nil {
@@ -147,6 +149,7 @@ func (m *Manifests) Generate(_ context.Context, dependencies asset.Parents) erro
147149
m.FileList = append(m.FileList, clusterCSIDriverConfig.Files()...)
148150
m.FileList = append(m.FileList, imageDigestMirrorSet.Files()...)
149151
m.FileList = append(m.FileList, bmcVerifyCAConfigMap.Files()...)
152+
m.FileList = append(m.FileList, pkiConfig.Files()...)
150153

151154
asset.SortFiles(m.FileList)
152155

pkg/asset/manifests/pki.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package manifests
2+
3+
import (
4+
"context"
5+
"path"
6+
7+
"github.com/pkg/errors"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"sigs.k8s.io/yaml"
10+
11+
configv1alpha1 "github.com/openshift/api/config/v1alpha1"
12+
features "github.com/openshift/api/features"
13+
"github.com/openshift/installer/pkg/asset"
14+
"github.com/openshift/installer/pkg/asset/installconfig"
15+
pkidefaults "github.com/openshift/installer/pkg/types/pki"
16+
)
17+
18+
var pkiCfgFilename = path.Join(manifestDir, "cluster-pki-02-config.yaml")
19+
20+
// PKIConfiguration generates the PKI custom resource manifest.
21+
type PKIConfiguration struct {
22+
FileList []*asset.File
23+
}
24+
25+
var _ asset.WritableAsset = (*PKIConfiguration)(nil)
26+
27+
// Name returns a human friendly name for the asset.
28+
func (*PKIConfiguration) Name() string {
29+
return "PKI Config"
30+
}
31+
32+
// Dependencies returns all of the dependencies directly needed to generate
33+
// the asset.
34+
func (*PKIConfiguration) Dependencies() []asset.Asset {
35+
return []asset.Asset{
36+
&installconfig.InstallConfig{},
37+
}
38+
}
39+
40+
// Generate generates the PKI custom resource manifest.
41+
// The manifest is only generated when the ConfigurablePKI feature gate is enabled.
42+
func (p *PKIConfiguration) Generate(_ context.Context, dependencies asset.Parents) error {
43+
installConfig := &installconfig.InstallConfig{}
44+
dependencies.Get(installConfig)
45+
46+
if !installConfig.Config.Enabled(features.FeatureGateConfigurablePKI) {
47+
return nil
48+
}
49+
50+
certMgmt := configv1alpha1.PKICertificateManagement{
51+
Mode: configv1alpha1.PKICertificateManagementModeDefault,
52+
}
53+
54+
if installConfig.Config.PKI != nil {
55+
profile := pkidefaults.DefaultPKIProfile()
56+
profile.SignerCertificates = pkidefaults.CertificateConfigToUpstream(installConfig.Config.PKI.SignerCertificates)
57+
58+
certMgmt = configv1alpha1.PKICertificateManagement{
59+
Mode: configv1alpha1.PKICertificateManagementModeCustom,
60+
Custom: configv1alpha1.CustomPKIPolicy{
61+
PKIProfile: profile,
62+
},
63+
}
64+
}
65+
66+
config := &configv1alpha1.PKI{
67+
TypeMeta: metav1.TypeMeta{
68+
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
69+
Kind: "PKI",
70+
},
71+
ObjectMeta: metav1.ObjectMeta{
72+
Name: "cluster",
73+
},
74+
Spec: configv1alpha1.PKISpec{
75+
CertificateManagement: certMgmt,
76+
},
77+
}
78+
79+
configData, err := yaml.Marshal(config)
80+
if err != nil {
81+
return errors.Wrapf(err, "failed to marshal PKI config")
82+
}
83+
84+
p.FileList = []*asset.File{
85+
{
86+
Filename: pkiCfgFilename,
87+
Data: configData,
88+
},
89+
}
90+
91+
return nil
92+
}
93+
94+
// Files returns the files generated by the asset.
95+
func (p *PKIConfiguration) Files() []*asset.File {
96+
return p.FileList
97+
}
98+
99+
// Load returns false since this asset is not written to disk by the installer.
100+
func (p *PKIConfiguration) Load(f asset.FileFetcher) (bool, error) {
101+
return false, nil
102+
}

pkg/asset/manifests/pki_test.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package manifests
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"sigs.k8s.io/yaml"
9+
10+
configv1 "github.com/openshift/api/config/v1"
11+
configv1alpha1 "github.com/openshift/api/config/v1alpha1"
12+
"github.com/openshift/installer/pkg/asset"
13+
"github.com/openshift/installer/pkg/asset/installconfig"
14+
"github.com/openshift/installer/pkg/types"
15+
)
16+
17+
func TestPKIConfigurationGenerate(t *testing.T) {
18+
cases := []struct {
19+
name string
20+
installConfig *types.InstallConfig
21+
expectEmpty bool
22+
expectMode configv1alpha1.PKICertificateManagementMode
23+
expectSignerAlgo configv1alpha1.KeyAlgorithm
24+
expectSignerRSA int32
25+
expectSignerCurve configv1alpha1.ECDSACurve
26+
expectDefaultsAlgo configv1alpha1.KeyAlgorithm
27+
expectDefaultsRSA int32
28+
expectDefaultsCurve configv1alpha1.ECDSACurve
29+
}{
30+
{
31+
name: "feature gate disabled - no manifest generated",
32+
installConfig: &types.InstallConfig{
33+
FeatureSet: configv1.Default,
34+
},
35+
expectEmpty: true,
36+
},
37+
{
38+
name: "feature gate enabled, pki nil - mode Default",
39+
installConfig: &types.InstallConfig{
40+
FeatureSet: configv1.TechPreviewNoUpgrade,
41+
},
42+
expectEmpty: false,
43+
expectMode: configv1alpha1.PKICertificateManagementModeDefault,
44+
},
45+
{
46+
name: "feature gate enabled, pki RSA-4096",
47+
installConfig: &types.InstallConfig{
48+
FeatureSet: configv1.TechPreviewNoUpgrade,
49+
PKI: &types.PKIConfig{
50+
SignerCertificates: types.CertificateConfig{
51+
Key: types.KeyConfig{
52+
Algorithm: types.KeyAlgorithmRSA,
53+
RSA: types.RSAKeyConfig{KeySize: 4096},
54+
},
55+
},
56+
},
57+
},
58+
expectEmpty: false,
59+
expectMode: configv1alpha1.PKICertificateManagementModeCustom,
60+
expectSignerAlgo: configv1alpha1.KeyAlgorithmRSA,
61+
expectSignerRSA: 4096,
62+
expectDefaultsAlgo: configv1alpha1.KeyAlgorithmRSA,
63+
expectDefaultsRSA: 4096,
64+
},
65+
{
66+
name: "feature gate enabled, pki ECDSA P-384",
67+
installConfig: &types.InstallConfig{
68+
FeatureSet: configv1.TechPreviewNoUpgrade,
69+
PKI: &types.PKIConfig{
70+
SignerCertificates: types.CertificateConfig{
71+
Key: types.KeyConfig{
72+
Algorithm: types.KeyAlgorithmECDSA,
73+
ECDSA: types.ECDSAKeyConfig{Curve: types.ECDSACurveP384},
74+
},
75+
},
76+
},
77+
},
78+
expectEmpty: false,
79+
expectMode: configv1alpha1.PKICertificateManagementModeCustom,
80+
expectSignerAlgo: configv1alpha1.KeyAlgorithmECDSA,
81+
expectSignerCurve: configv1alpha1.ECDSACurveP384,
82+
expectDefaultsAlgo: configv1alpha1.KeyAlgorithmRSA,
83+
expectDefaultsRSA: 4096,
84+
},
85+
{
86+
name: "feature gate enabled, pki RSA-2048 explicit",
87+
installConfig: &types.InstallConfig{
88+
FeatureSet: configv1.TechPreviewNoUpgrade,
89+
PKI: &types.PKIConfig{
90+
SignerCertificates: types.CertificateConfig{
91+
Key: types.KeyConfig{
92+
Algorithm: types.KeyAlgorithmRSA,
93+
RSA: types.RSAKeyConfig{KeySize: 2048},
94+
},
95+
},
96+
},
97+
},
98+
expectEmpty: false,
99+
expectMode: configv1alpha1.PKICertificateManagementModeCustom,
100+
expectSignerAlgo: configv1alpha1.KeyAlgorithmRSA,
101+
expectSignerRSA: 2048,
102+
expectDefaultsAlgo: configv1alpha1.KeyAlgorithmRSA,
103+
expectDefaultsRSA: 4096,
104+
},
105+
}
106+
107+
for _, tc := range cases {
108+
t.Run(tc.name, func(t *testing.T) {
109+
parents := asset.Parents{}
110+
parents.Add(installconfig.MakeAsset(tc.installConfig))
111+
112+
pkiAsset := &PKIConfiguration{}
113+
err := pkiAsset.Generate(context.Background(), parents)
114+
if !assert.NoError(t, err) {
115+
return
116+
}
117+
118+
if tc.expectEmpty {
119+
assert.Empty(t, pkiAsset.Files())
120+
return
121+
}
122+
123+
if !assert.Len(t, pkiAsset.Files(), 1) {
124+
return
125+
}
126+
assert.Equal(t, "manifests/cluster-pki-02-config.yaml", pkiAsset.Files()[0].Filename)
127+
128+
// Unmarshal and verify the CR structure
129+
var pkiCR configv1alpha1.PKI
130+
err = yaml.Unmarshal(pkiAsset.Files()[0].Data, &pkiCR)
131+
if !assert.NoError(t, err) {
132+
return
133+
}
134+
135+
assert.Equal(t, "config.openshift.io/v1alpha1", pkiCR.APIVersion)
136+
assert.Equal(t, "PKI", pkiCR.Kind)
137+
assert.Equal(t, "cluster", pkiCR.Name)
138+
assert.Equal(t, tc.expectMode, pkiCR.Spec.CertificateManagement.Mode)
139+
140+
if tc.expectMode != configv1alpha1.PKICertificateManagementModeCustom {
141+
return
142+
}
143+
144+
profile := pkiCR.Spec.CertificateManagement.Custom.PKIProfile
145+
146+
// Verify defaults
147+
assert.Equal(t, tc.expectDefaultsAlgo, profile.Defaults.Key.Algorithm)
148+
if tc.expectDefaultsAlgo == configv1alpha1.KeyAlgorithmECDSA {
149+
assert.Equal(t, tc.expectDefaultsCurve, profile.Defaults.Key.ECDSA.Curve)
150+
} else {
151+
assert.Equal(t, tc.expectDefaultsRSA, profile.Defaults.Key.RSA.KeySize)
152+
}
153+
154+
// Verify signerCertificates
155+
assert.Equal(t, tc.expectSignerAlgo, profile.SignerCertificates.Key.Algorithm)
156+
if tc.expectSignerAlgo == configv1alpha1.KeyAlgorithmRSA {
157+
assert.Equal(t, tc.expectSignerRSA, profile.SignerCertificates.Key.RSA.KeySize)
158+
}
159+
if tc.expectSignerAlgo == configv1alpha1.KeyAlgorithmECDSA {
160+
assert.Equal(t, tc.expectSignerCurve, profile.SignerCertificates.Key.ECDSA.Curve)
161+
}
162+
})
163+
}
164+
}

pkg/explain/printer_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ the cluster.
139139
Valid Values: "rhel-9","rhel-10"
140140
OSImageStream is the global OS Image Stream to be used for all machines in the cluster.
141141
142+
pki <object>
143+
PKI configures cryptographic parameters for installer-generated
144+
signer certificates. When specified, all signer certificates use the
145+
algorithm and parameters from signerCertificates.
146+
Feature gated by ConfigurablePKI.
147+
142148
platform <object> -required-
143149
Platform is the configuration for the specific platform upon which to
144150
perform the installation.

pkg/types/defaults/installconfig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,5 @@ func SetInstallConfigDefaults(c *types.InstallConfig) {
142142
if c.AdditionalTrustBundlePolicy == "" {
143143
c.AdditionalTrustBundlePolicy = types.PolicyProxyOnly
144144
}
145+
145146
}

0 commit comments

Comments
 (0)