Skip to content
Open
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
76 changes: 76 additions & 0 deletions data/data/install.openshift.io_installconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5084,6 +5084,82 @@ spec:
- rhel-9
- rhel-10
type: string
pki:
description: |-
PKI configures cryptographic parameters for installer-generated
signer certificates. When specified, all signer certificates use the
algorithm and parameters from signerCertificates.
Feature gated by ConfigurablePKI.
Comment on lines +5087 to +5092

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't publish spec.pki as active before signer generation consumes it.

This schema says all installer-generated signer certificates use signerCertificates, but the supplied PR context says signer wiring is still deferred and current behavior remains RSA-2048 via nil PKI config. That makes the CRD/oc explain contract misleading for anyone enabling ConfigurablePKI. Either hold this field until the follow-up lands, or soften the description so it does not claim behavior that is not implemented yet.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/data/install.openshift.io_installconfigs.yaml` around lines 5087 - 5092,
The field description for spec.pki currently asserts that installer-generated
signer certificates use signerCertificates (and implies the feature is active
under ConfigurablePKI) which is untrue until signer wiring is implemented;
update the description or withhold publishing spec.pki: either (a) soften the
text to state that signerCertificates will be used once the signer wiring is
implemented and that current behavior defaults to RSA-2048 when PKI is nil,
referencing spec.pki and signerCertificates and the ConfigurablePKI feature
gate, or (b) remove/hold the spec.pki entry from the published schema until the
follow-up that wires signer generation lands so oc explain does not advertise
unimplemented behavior.

properties:
signerCertificates:
description: |-
signerCertificates specifies key parameters for all installer-generated
certificate authority (CA) certificates.
When set, all signer certificates use the specified algorithm and parameters.
minProperties: 1
properties:
key:
description: key specifies the cryptographic parameters for the
certificate's key pair.
properties:
algorithm:
description: |-
algorithm specifies the key generation algorithm.
Valid values are "RSA" and "ECDSA".
enum:
- RSA
- ECDSA
type: string
ecdsa:
description: |-
ecdsa specifies ECDSA key parameters.
Required when algorithm is ECDSA, and forbidden otherwise.
properties:
curve:
description: |-
curve specifies the NIST elliptic curve for ECDSA keys.
Valid values are "P256", "P384", and "P521".
enum:
- P256
- P384
- P521
type: string
required:
- curve
type: object
rsa:
description: |-
rsa specifies RSA key parameters.
Required when algorithm is RSA, and forbidden otherwise.
properties:
keySize:
description: |-
keySize specifies the size of RSA keys in bits.
Valid values are multiples of 1024 from 2048 to 8192.
format: int32
maximum: 8192
minimum: 2048
multipleOf: 1024
type: integer
required:
- keySize
type: object
required:
- algorithm
type: object
x-kubernetes-validations:
- message: rsa is required when algorithm is RSA, and forbidden
otherwise
rule: 'has(self.algorithm) && self.algorithm == ''RSA'' ? has(self.rsa)
: !has(self.rsa)'
- message: ecdsa is required when algorithm is ECDSA, and forbidden
otherwise
rule: 'has(self.algorithm) && self.algorithm == ''ECDSA'' ? has(self.ecdsa)
: !has(self.ecdsa)'
type: object
required:
- signerCertificates
type: object
platform:
description: |-
Platform is the configuration for the specific platform upon which to
Expand Down
5 changes: 4 additions & 1 deletion pkg/asset/imagebased/configimage/ingressoperatorsigner.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ func (a *IngressOperatorSignerCertKey) Generate(ctx context.Context, dependencie
return err
}

a.KeyRaw = tls.PrivateKeyToPem(key)
a.KeyRaw, err = tls.PrivateKeyToPem(key)
if err != nil {
return fmt.Errorf("failed to encode private key to PEM: %w", err)
}
a.CertRaw = tls.CertToPem(crt)

return nil
Expand Down
5 changes: 4 additions & 1 deletion pkg/asset/manifests/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ func (m *Manifests) Dependencies() []asset.Asset {
&bootkube.InternalReleaseImageTLSSecret{},
&bootkube.InternalReleaseImageRegistryAuthSecret{},
&BMCVerifyCAConfigMap{},
&PKIConfiguration{},
}
}

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

dependencies.Get(installConfig, ingress, dns, network, infra, proxy, scheduler, imageContentSourcePolicy, imageDigestMirrorSet, clusterCSIDriverConfig, mcoCfgTemplate, bmcVerifyCAConfigMap)
dependencies.Get(installConfig, ingress, dns, network, infra, proxy, scheduler, imageContentSourcePolicy, imageDigestMirrorSet, clusterCSIDriverConfig, mcoCfgTemplate, bmcVerifyCAConfigMap, pkiConfig)

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

asset.SortFiles(m.FileList)

Expand Down
102 changes: 102 additions & 0 deletions pkg/asset/manifests/pki.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package manifests

import (
"context"
"path"

"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"

configv1alpha1 "github.com/openshift/api/config/v1alpha1"
features "github.com/openshift/api/features"
"github.com/openshift/installer/pkg/asset"
"github.com/openshift/installer/pkg/asset/installconfig"
pkidefaults "github.com/openshift/installer/pkg/types/pki"
)

var pkiCfgFilename = path.Join(manifestDir, "cluster-pki-02-config.yaml")

// PKIConfiguration generates the PKI custom resource manifest.
type PKIConfiguration struct {
FileList []*asset.File
}

var _ asset.WritableAsset = (*PKIConfiguration)(nil)

// Name returns a human friendly name for the asset.
func (*PKIConfiguration) Name() string {
return "PKI Config"
}

// Dependencies returns all of the dependencies directly needed to generate
// the asset.
func (*PKIConfiguration) Dependencies() []asset.Asset {
return []asset.Asset{
&installconfig.InstallConfig{},
}
}

// Generate generates the PKI custom resource manifest.
// The manifest is only generated when the ConfigurablePKI feature gate is enabled.
func (p *PKIConfiguration) Generate(_ context.Context, dependencies asset.Parents) error {
installConfig := &installconfig.InstallConfig{}
dependencies.Get(installConfig)

if !installConfig.Config.Enabled(features.FeatureGateConfigurablePKI) {
return nil
}

certMgmt := configv1alpha1.PKICertificateManagement{
Mode: configv1alpha1.PKICertificateManagementModeDefault,
}

if installConfig.Config.PKI != nil {
profile := pkidefaults.DefaultPKIProfile()
profile.SignerCertificates = pkidefaults.CertificateConfigToUpstream(installConfig.Config.PKI.SignerCertificates)

certMgmt = configv1alpha1.PKICertificateManagement{
Mode: configv1alpha1.PKICertificateManagementModeCustom,
Custom: configv1alpha1.CustomPKIPolicy{
PKIProfile: profile,
},
}
}

config := &configv1alpha1.PKI{
TypeMeta: metav1.TypeMeta{
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
Kind: "PKI",
},
ObjectMeta: metav1.ObjectMeta{
Name: "cluster",
},
Spec: configv1alpha1.PKISpec{
CertificateManagement: certMgmt,
},
}

configData, err := yaml.Marshal(config)
if err != nil {
return errors.Wrapf(err, "failed to marshal PKI config")
}

p.FileList = []*asset.File{
{
Filename: pkiCfgFilename,
Data: configData,
},
}

return nil
}

// Files returns the files generated by the asset.
func (p *PKIConfiguration) Files() []*asset.File {
return p.FileList
}

// Load returns false since this asset is not written to disk by the installer.
func (p *PKIConfiguration) Load(f asset.FileFetcher) (bool, error) {
return false, nil
}
164 changes: 164 additions & 0 deletions pkg/asset/manifests/pki_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package manifests

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"sigs.k8s.io/yaml"

configv1 "github.com/openshift/api/config/v1"
configv1alpha1 "github.com/openshift/api/config/v1alpha1"
"github.com/openshift/installer/pkg/asset"
"github.com/openshift/installer/pkg/asset/installconfig"
"github.com/openshift/installer/pkg/types"
)

func TestPKIConfigurationGenerate(t *testing.T) {
cases := []struct {
name string
installConfig *types.InstallConfig
expectEmpty bool
expectMode configv1alpha1.PKICertificateManagementMode
expectSignerAlgo configv1alpha1.KeyAlgorithm
expectSignerRSA int32
expectSignerCurve configv1alpha1.ECDSACurve
expectDefaultsAlgo configv1alpha1.KeyAlgorithm
expectDefaultsRSA int32
expectDefaultsCurve configv1alpha1.ECDSACurve
}{
{
name: "feature gate disabled - no manifest generated",
installConfig: &types.InstallConfig{
FeatureSet: configv1.Default,
},
expectEmpty: true,
},
{
name: "feature gate enabled, pki nil - mode Default",
installConfig: &types.InstallConfig{
FeatureSet: configv1.TechPreviewNoUpgrade,
},
expectEmpty: false,
expectMode: configv1alpha1.PKICertificateManagementModeDefault,
},
{
name: "feature gate enabled, pki RSA-4096",
installConfig: &types.InstallConfig{
FeatureSet: configv1.TechPreviewNoUpgrade,
PKI: &types.PKIConfig{
SignerCertificates: types.CertificateConfig{
Key: types.KeyConfig{
Algorithm: types.KeyAlgorithmRSA,
RSA: types.RSAKeyConfig{KeySize: 4096},
},
},
},
},
expectEmpty: false,
expectMode: configv1alpha1.PKICertificateManagementModeCustom,
expectSignerAlgo: configv1alpha1.KeyAlgorithmRSA,
expectSignerRSA: 4096,
expectDefaultsAlgo: configv1alpha1.KeyAlgorithmRSA,
expectDefaultsRSA: 4096,
},
{
name: "feature gate enabled, pki ECDSA P-384",
installConfig: &types.InstallConfig{
FeatureSet: configv1.TechPreviewNoUpgrade,
PKI: &types.PKIConfig{
SignerCertificates: types.CertificateConfig{
Key: types.KeyConfig{
Algorithm: types.KeyAlgorithmECDSA,
ECDSA: types.ECDSAKeyConfig{Curve: types.ECDSACurveP384},
},
},
},
},
expectEmpty: false,
expectMode: configv1alpha1.PKICertificateManagementModeCustom,
expectSignerAlgo: configv1alpha1.KeyAlgorithmECDSA,
expectSignerCurve: configv1alpha1.ECDSACurveP384,
expectDefaultsAlgo: configv1alpha1.KeyAlgorithmRSA,
expectDefaultsRSA: 4096,
},
{
name: "feature gate enabled, pki RSA-2048 explicit",
installConfig: &types.InstallConfig{
FeatureSet: configv1.TechPreviewNoUpgrade,
PKI: &types.PKIConfig{
SignerCertificates: types.CertificateConfig{
Key: types.KeyConfig{
Algorithm: types.KeyAlgorithmRSA,
RSA: types.RSAKeyConfig{KeySize: 2048},
},
},
},
},
expectEmpty: false,
expectMode: configv1alpha1.PKICertificateManagementModeCustom,
expectSignerAlgo: configv1alpha1.KeyAlgorithmRSA,
expectSignerRSA: 2048,
expectDefaultsAlgo: configv1alpha1.KeyAlgorithmRSA,
expectDefaultsRSA: 4096,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
parents := asset.Parents{}
parents.Add(installconfig.MakeAsset(tc.installConfig))

pkiAsset := &PKIConfiguration{}
err := pkiAsset.Generate(context.Background(), parents)
if !assert.NoError(t, err) {
return
}

if tc.expectEmpty {
assert.Empty(t, pkiAsset.Files())
return
}

if !assert.Len(t, pkiAsset.Files(), 1) {
return
}
assert.Equal(t, "manifests/cluster-pki-02-config.yaml", pkiAsset.Files()[0].Filename)

// Unmarshal and verify the CR structure
var pkiCR configv1alpha1.PKI
err = yaml.Unmarshal(pkiAsset.Files()[0].Data, &pkiCR)
if !assert.NoError(t, err) {
return
}

assert.Equal(t, "config.openshift.io/v1alpha1", pkiCR.APIVersion)
assert.Equal(t, "PKI", pkiCR.Kind)
assert.Equal(t, "cluster", pkiCR.Name)
assert.Equal(t, tc.expectMode, pkiCR.Spec.CertificateManagement.Mode)

if tc.expectMode != configv1alpha1.PKICertificateManagementModeCustom {
return
}

profile := pkiCR.Spec.CertificateManagement.Custom.PKIProfile

// Verify defaults
assert.Equal(t, tc.expectDefaultsAlgo, profile.Defaults.Key.Algorithm)
if tc.expectDefaultsAlgo == configv1alpha1.KeyAlgorithmECDSA {
assert.Equal(t, tc.expectDefaultsCurve, profile.Defaults.Key.ECDSA.Curve)
} else {
assert.Equal(t, tc.expectDefaultsRSA, profile.Defaults.Key.RSA.KeySize)
}

// Verify signerCertificates
assert.Equal(t, tc.expectSignerAlgo, profile.SignerCertificates.Key.Algorithm)
if tc.expectSignerAlgo == configv1alpha1.KeyAlgorithmRSA {
assert.Equal(t, tc.expectSignerRSA, profile.SignerCertificates.Key.RSA.KeySize)
}
if tc.expectSignerAlgo == configv1alpha1.KeyAlgorithmECDSA {
assert.Equal(t, tc.expectSignerCurve, profile.SignerCertificates.Key.ECDSA.Curve)
}
})
}
}
Loading