Skip to content

Commit

Permalink
Merge pull request #9128 from mresvanis/mgmt-19151-fix-ingress-ca
Browse files Browse the repository at this point in the history
MGMT-19151: Add ingress CN to cluster configuration for image-based installer
  • Loading branch information
openshift-merge-bot[bot] authored Oct 23, 2024
2 parents 8032a54 + b0c45d4 commit 694d083
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 11 deletions.
24 changes: 23 additions & 1 deletion pkg/asset/imagebased/configimage/clusterconfiguration.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package configimage

import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -131,6 +133,11 @@ func (cc *ClusterConfiguration) Generate(_ context.Context, dependencies asset.P
}
}

ingressCertificateCN, err := getCommonNameFromCertificate(ingressOperatorSignerCertKey.Cert())
if err != nil {
return fmt.Errorf("failed to get CN from ingress CA certificate: %w", err)
}

cc.Config.KubeconfigCryptoRetention = imagebased.KubeConfigCryptoRetention{
KubeAPICrypto: imagebased.KubeAPICrypto{
ServingCrypto: imagebased.ServingCrypto{
Expand All @@ -143,7 +150,8 @@ func (cc *ClusterConfiguration) Generate(_ context.Context, dependencies asset.P
},
},
IngresssCrypto: imagebased.IngresssCrypto{
IngressCA: string(ingressOperatorSignerCertKey.Key()),
IngressCAPrivateKey: string(ingressOperatorSignerCertKey.Key()),
IngressCertificateCN: ingressCertificateCN,
},
}

Expand Down Expand Up @@ -214,3 +222,17 @@ func chronyConfWithAdditionalNTPSources(sources []string) string {
}
return content
}

func getCommonNameFromCertificate(certPEM []byte) (string, error) {
block, _ := pem.Decode(certPEM)
if block == nil {
return "", fmt.Errorf("failed to decode PEM block")
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", fmt.Errorf("failed to parse certificate: %w", err)
}

return cert.Subject.CommonName, nil
}
49 changes: 46 additions & 3 deletions pkg/asset/imagebased/configimage/clusterconfiguration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@ package configimage

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"os"
"testing"
"time"

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
Expand All @@ -23,6 +32,8 @@ const (
testSSHKey = "ssh-rsa AAAAB3NzaC1y1LJe3zew1ghc= [email protected]"

testSecret = `{"auths":{"cloud.openshift.com":{"auth":"b3BlUTA=","email":"[email protected]"}}}` //nolint:gosec // not real credentials

testIngressCN = "ingress-operator@482023856"
)

func TestClusterConfiguration_Generate(t *testing.T) {
Expand Down Expand Up @@ -226,7 +237,8 @@ func TestClusterConfiguration_LoadedFromDisk(t *testing.T) {
}
},
"IngresssCrypto": {
"ingress_ca": "ingress-key"
"ingress_ca": "ingress-key",
"ingress_certificate_cn": "` + testIngressCN + `"
}
},
"ssh_key": "ssh-rsa AAAAB3NzaC1y1LJe3zew1ghc= [email protected]",
Expand Down Expand Up @@ -346,7 +358,8 @@ func clusterConfiguration() *ClusterConfigurationBuilder {
},
},
IngresssCrypto: imagebased.IngresssCrypto{
IngressCA: string(ingressCertKey().SelfSignedCertKey.Key()),
IngressCAPrivateKey: string(ingressCertKey().SelfSignedCertKey.Key()),
IngressCertificateCN: testIngressCN,
},
}

Expand Down Expand Up @@ -431,10 +444,25 @@ func adminKubeConfigCertKey() *tls.AdminKubeConfigSignerCertKey {
}

func ingressCertKey() *IngressOperatorSignerCertKey {
crt := &x509.Certificate{
Subject: pkix.Name{
CommonName: testIngressCN,
},
SerialNumber: big.NewInt(42),
NotBefore: time.Now(),
NotAfter: time.Now().Add(1 * time.Hour),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
}
block, _, _ := genSelfSignedKeyPair(crt) //nolint:errcheck
crtPEM := pem.EncodeToMemory(block)

return &IngressOperatorSignerCertKey{
SelfSignedCertKey: tls.SelfSignedCertKey{
CertKey: tls.CertKey{
CertRaw: []byte("ingress-cert"),
CertRaw: crtPEM,
KeyRaw: []byte("ingress-key"),
},
},
Expand Down Expand Up @@ -469,3 +497,18 @@ func installConfig() *InstallConfigBuilder {
InstallConfig: *defaultInstallConfig(),
}
}

// genSelfSignedKeyPair generates a key and a self-signed certificate from the provided Certificate.
func genSelfSignedKeyPair(cert *x509.Certificate) (*pem.Block, *ecdsa.PrivateKey, error) {
key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("could not generate rsa key - %w", err)
}

signedCert, err := x509.CreateCertificate(rand.Reader, cert, cert, &key.PublicKey, key)
if err != nil {
return nil, nil, fmt.Errorf("could not generate self-signed certificate - %w", err)
}

return &pem.Block{Type: "CERTIFICATE", Bytes: signedCert}, key, err
}
100 changes: 98 additions & 2 deletions pkg/asset/imagebased/configimage/ingressoperatorsigner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ package configimage

import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha1" //nolint: gosec
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"math"
"math/big"
"time"

"github.com/openshift/installer/pkg/asset"
Expand Down Expand Up @@ -34,13 +44,21 @@ func (a *IngressOperatorSignerCertKey) Generate(ctx context.Context, dependencie
signerName := fmt.Sprintf("%s@%d", "ingress-operator", time.Now().Unix())

cfg := &tls.CertCfg{
Subject: pkix.Name{CommonName: signerName, OrganizationalUnit: []string{"openshift"}},
Subject: pkix.Name{CommonName: signerName},
KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
Validity: tls.ValidityOneYear * 2,
IsCA: true,
}

return a.SelfSignedCertKey.Generate(ctx, cfg, "ingress-operator-signer")
key, crt, err := generateSelfSignedCertificate(cfg)
if err != nil {
return err
}

a.KeyRaw = tls.PrivateKeyToPem(key)
a.CertRaw = tls.CertToPem(crt)

return nil
}

// IngressOperatorCABundle is the asset the generates the ingress-operator-signer-ca-bundle,
Expand Down Expand Up @@ -72,3 +90,81 @@ func (a *IngressOperatorCABundle) Generate(ctx context.Context, deps asset.Paren
func (a *IngressOperatorCABundle) Name() string {
return "Certificate (ingress-operator-ca-bundle)"
}

// selfSignedCertificate creates a self signed certificate.
//
// TODO: this is a modified tls.SelfSignedCertificate function to allow the
// certificate's Subject.OU to be empty. We should revisit this by sharing as
// much code as possible with the tls package.
//
// https://github.com/openshift/installer/blob/6723dfd18056a6d002f792afce5547fc24874908/pkg/asset/tls/tls.go
func selfSignedCertificate(cfg *tls.CertCfg, key *rsa.PrivateKey) (*x509.Certificate, error) {
serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64))
if err != nil {
return nil, err
}
cert := x509.Certificate{
BasicConstraintsValid: true,
IsCA: cfg.IsCA,
KeyUsage: cfg.KeyUsages,
NotAfter: time.Now().Add(cfg.Validity),
NotBefore: time.Now(),
SerialNumber: serial,
Subject: cfg.Subject,
}
// verifies that the CN for the cert is set
if len(cfg.Subject.CommonName) == 0 {
return nil, errors.New("certification's subject is not set, or invalid")
}
pub := key.Public()
cert.SubjectKeyId, err = generateSubjectKeyID(pub)
if err != nil {
return nil, fmt.Errorf("failed to set subject key identifier: %w", err)
}
certBytes, err := x509.CreateCertificate(rand.Reader, &cert, &cert, key.Public(), key)
if err != nil {
return nil, fmt.Errorf("failed to create certificate: %w", err)
}
return x509.ParseCertificate(certBytes)
}

// generateSelfSignedCertificate generates a key/cert pair defined by CertCfg.
func generateSelfSignedCertificate(cfg *tls.CertCfg) (*rsa.PrivateKey, *x509.Certificate, error) {
key, err := tls.PrivateKey()
if err != nil {
return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
}

crt, err := selfSignedCertificate(cfg, key)
if err != nil {
return nil, nil, fmt.Errorf("failed to create self-signed certificate: %w", err)
}
return key, crt, nil
}

// rsaPublicKey reflects the ASN.1 structure of a PKCS#1 public key.
type rsaPublicKey struct {
N *big.Int
E int
}

// generateSubjectKeyID generates a SHA-1 hash of the subject public key.
func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) {
var publicKeyBytes []byte
var err error

switch pub := pub.(type) {
case *rsa.PublicKey:
publicKeyBytes, err = asn1.Marshal(rsaPublicKey{N: pub.N, E: pub.E})
if err != nil {
return nil, fmt.Errorf("failed to Marshal ans1 public key: %w", err)
}
case *ecdsa.PublicKey:
publicKeyBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y) //nolint: staticcheck
default:
return nil, errors.New("only RSA and ECDSA public keys supported")
}

hash := sha1.Sum(publicKeyBytes) //nolint: gosec
return hash[:], nil
}
13 changes: 8 additions & 5 deletions pkg/types/imagebased/seedreconfiguration.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ type KubeAPICrypto struct {
// ServingCrypto contains the kubernetes API private keys that are used to
// generate the cluster's certificates.
type ServingCrypto struct {
// LocalhostSignerPrivateKey is a PEM-encoded X.509 key.
// LocalhostSignerPrivateKey is a PEM-encoded private key.
LocalhostSignerPrivateKey string `json:"localhost_signer_private_key,omitempty"`

// ServiceNetworkSignerPrivateKey is a PEM-encoded X.509 key.
// ServiceNetworkSignerPrivateKey is a PEM-encoded private key.
ServiceNetworkSignerPrivateKey string `json:"service_network_signer_private_key,omitempty"`

// LoadbalancerSignerPrivateKey is a PEM-encoded X.509 key.
// LoadbalancerSignerPrivateKey is a PEM-encoded private key.
LoadbalancerSignerPrivateKey string `json:"loadbalancer_external_signer_private_key,omitempty"`
}

Expand All @@ -125,8 +125,11 @@ type ClientAuthCrypto struct {

// IngresssCrypto contains the ingrees CA certificate.
type IngresssCrypto struct {
// IngressCA is a PEM-encoded X.509 certificate.
IngressCA string `json:"ingress_ca,omitempty"`
// IngressCAPrivateKey is a PEM-encoded private key.
IngressCAPrivateKey string `json:"ingress_ca,omitempty"`

// IngressCertificateCN is the Subject.CN of the ingress CA certificate.
IngressCertificateCN string `json:"ingress_certificate_cn,omitempty"`
}

// AdditionalTrustBundle represents the PEM-encoded X.509 certificate bundle
Expand Down

0 comments on commit 694d083

Please sign in to comment.