Skip to content

Commit 9f154cf

Browse files
committed
feat(csv): add generated APIService resource check
- Adds check for existence and validity of generated owned APIService resources in succeeded CSV phase - Adds transition from succeeded phase to pending on non-existent/invalid generated APIService resources - Refactors cert expiration info in CSV spec and status
1 parent ed395e0 commit 9f154cf

File tree

11 files changed

+386
-285
lines changed

11 files changed

+386
-285
lines changed

deploy/chart/templates/0000_30_02-clusterserviceversion.crd.yaml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -180,17 +180,7 @@ spec:
180180
values:
181181
type: array
182182
description: set of values for the expression
183-
certValidForDays:
184-
type: integer
185-
description: Number of days generated APIService certs are valid for
186-
format: int32
187-
minimum: 1
188-
maximum: 730
189-
certMinFreshSeconds:
190-
type: integer
191-
description: Minimum freshness of a cert before it is renewed; in seconds
192-
format: int32
193-
minimum: 10
183+
194184
apiservicedefinitions:
195185
type: object
196186
properties:

pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,6 @@ type ClusterServiceVersionSpec struct {
135135
// Label selector for related resources.
136136
// +optional
137137
Selector *metav1.LabelSelector `json:"selector,omitempty" protobuf:"bytes,2,opt,name=selector"`
138-
139-
// Number of days generated owned APIService certs are valid for
140-
// +optional
141-
CertValidForDays int `json:"certValidFor,omitempty"`
142-
143-
// Minimum freshness of a cert before it is renewed; in seconds
144-
// +optional
145-
CertMinFreshSeconds int `json:"certMinFresh,omitempty"`
146138
}
147139

148140
type Maintainer struct {
@@ -189,19 +181,20 @@ const (
189181
type ConditionReason string
190182

191183
const (
192-
CSVReasonRequirementsUnknown ConditionReason = "RequirementsUnknown"
193-
CSVReasonRequirementsNotMet ConditionReason = "RequirementsNotMet"
194-
CSVReasonRequirementsMet ConditionReason = "AllRequirementsMet"
195-
CSVReasonOwnerConflict ConditionReason = "OwnerConflict"
196-
CSVReasonComponentFailed ConditionReason = "InstallComponentFailed"
197-
CSVReasonInvalidStrategy ConditionReason = "InvalidInstallStrategy"
198-
CSVReasonWaiting ConditionReason = "InstallWaiting"
199-
CSVReasonInstallSuccessful ConditionReason = "InstallSucceeded"
200-
CSVReasonInstallCheckFailed ConditionReason = "InstallCheckFailed"
201-
CSVReasonComponentUnhealthy ConditionReason = "ComponentUnhealthy"
202-
CSVReasonBeingReplaced ConditionReason = "BeingReplaced"
203-
CSVReasonReplaced ConditionReason = "Replaced"
204-
CSVReasonNeedCertRefresh ConditionReason = "NeedCertRefresh"
184+
CSVReasonRequirementsUnknown ConditionReason = "RequirementsUnknown"
185+
CSVReasonRequirementsNotMet ConditionReason = "RequirementsNotMet"
186+
CSVReasonRequirementsMet ConditionReason = "AllRequirementsMet"
187+
CSVReasonOwnerConflict ConditionReason = "OwnerConflict"
188+
CSVReasonComponentFailed ConditionReason = "InstallComponentFailed"
189+
CSVReasonInvalidStrategy ConditionReason = "InvalidInstallStrategy"
190+
CSVReasonWaiting ConditionReason = "InstallWaiting"
191+
CSVReasonInstallSuccessful ConditionReason = "InstallSucceeded"
192+
CSVReasonInstallCheckFailed ConditionReason = "InstallCheckFailed"
193+
CSVReasonComponentUnhealthy ConditionReason = "ComponentUnhealthy"
194+
CSVReasonBeingReplaced ConditionReason = "BeingReplaced"
195+
CSVReasonReplaced ConditionReason = "Replaced"
196+
CSVReasonNeedCertRotation ConditionReason = "NeedCertRotation"
197+
CSVReasonAPIServiceResourceIssue ConditionReason = "APIServiceResourceIssue"
205198
)
206199

207200
// Conditions appear in the status as a record of state transitions on the ClusterServiceVersion
@@ -287,9 +280,12 @@ type ClusterServiceVersionStatus struct {
287280
Conditions []ClusterServiceVersionCondition `json:"conditions,omitempty"`
288281
// The status of each requirement for this CSV
289282
RequirementStatus []RequirementStatus `json:"requirementStatus,omitempty"`
290-
// Time to refresh generated owned APIService certs
283+
// Last time the owned APIService certs were updated
284+
// +optional
285+
CertsLastUpdated metav1.Time `json:"certsLastUpdated,omitempty"`
286+
// Time the owned APIService certs will rotate next
291287
// +optional
292-
CertRefresh metav1.Time `json:"certRefresh,omitempty"`
288+
CertsRotateAt metav1.Time `json:"certsRotateAt,omitempty"`
293289
}
294290

295291
// ClusterServiceVersion is a Custom Resource of type `ClusterServiceVersionSpec`.

pkg/api/apis/operators/v1alpha1/zz_generated.deepcopy.go

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/controller/certs/certs.go

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@ import (
44
"crypto/ecdsa"
55
"crypto/elliptic"
66
"crypto/rand"
7+
"crypto/sha256"
78
"crypto/x509"
89
"crypto/x509/pkix"
10+
"encoding/hex"
911
"encoding/pem"
1012
"fmt"
13+
"math"
1114
"math/big"
1215
"time"
1316
)
1417

18+
const (
19+
Organization = "Red Hat, Inc."
20+
)
21+
1522
// KeyPair stores an x509 certificate and its ECDSA private key
1623
type KeyPair struct {
1724
Cert *x509.Certificate
@@ -42,19 +49,21 @@ func (kp *KeyPair) ToPEM() (certPEM []byte, privPEM []byte, err error) {
4249
}
4350

4451
// GenerateCA generates a self-signed CA cert/key pair that expires in expiresIn days
45-
func GenerateCA(expiresIn int) (*KeyPair, time.Time, error) {
46-
if expiresIn > 730 || expiresIn <= 0 {
47-
return nil, time.Time{}, fmt.Errorf("invalid cert expiration")
52+
func GenerateCA(notAfter time.Time) (*KeyPair, error) {
53+
notBefore := time.Now()
54+
if notAfter.Before(notBefore) {
55+
return nil, fmt.Errorf("invalid notAfter: %s before %s", notAfter.String(), notBefore.String())
4856
}
4957

50-
notBefore := time.Now()
51-
notAfter := notBefore.AddDate(0, 0, expiresIn)
58+
serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64))
59+
if err != nil {
60+
return nil, err
61+
}
5262

5363
caDetails := &x509.Certificate{
54-
//TODO(Nick): figure out what to use for a SerialNumber
55-
SerialNumber: big.NewInt(1653),
64+
SerialNumber: serial,
5665
Subject: pkix.Name{
57-
Organization: []string{"Red Hat, Inc."},
66+
Organization: []string{Organization},
5867
},
5968
NotBefore: notBefore,
6069
NotAfter: notAfter,
@@ -66,43 +75,47 @@ func GenerateCA(expiresIn int) (*KeyPair, time.Time, error) {
6675

6776
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
6877
if err != nil {
69-
return nil, time.Time{}, err
78+
return nil, err
7079
}
7180

7281
publicKey := &privateKey.PublicKey
7382
certRaw, err := x509.CreateCertificate(rand.Reader, caDetails, caDetails, publicKey, privateKey)
7483
if err != nil {
75-
return nil, time.Time{}, err
84+
return nil, err
7685
}
7786

7887
cert, err := x509.ParseCertificate(certRaw)
7988
if err != nil {
80-
return nil, time.Time{}, err
89+
return nil, err
8190
}
8291

8392
ca := &KeyPair{
8493
Cert: cert,
8594
Priv: privateKey,
8695
}
8796

88-
return ca, notAfter, nil
97+
return ca, nil
8998
}
9099

91100
// CreateSignedServingPair creates a serving cert/key pair signed by the given ca
92-
func CreateSignedServingPair(expiresIn int, ca *KeyPair, hosts []string) (*KeyPair, error) {
93-
if expiresIn > 730 || expiresIn <= 0 {
94-
return nil, fmt.Errorf("invalid cert expiration")
101+
func CreateSignedServingPair(notAfter time.Time, ca *KeyPair, hosts []string) (*KeyPair, error) {
102+
notBefore := time.Now()
103+
if notAfter.Before(notBefore) {
104+
return nil, fmt.Errorf("invalid notAfter: %s before %s", notAfter.String(), notBefore.String())
105+
}
106+
107+
serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64))
108+
if err != nil {
109+
return nil, err
95110
}
96111

97112
certDetails := &x509.Certificate{
98-
//TODO(Nick): figure out what to use for a SerialNumber
99-
SerialNumber: big.NewInt(1653),
113+
SerialNumber: serial,
100114
Subject: pkix.Name{
101-
Organization: []string{"Red Hat, Inc."},
115+
Organization: []string{Organization},
102116
},
103-
NotBefore: time.Now(),
104-
// Valid for 2 years
105-
NotAfter: time.Now().AddDate(0, 0, expiresIn),
117+
NotBefore: notBefore,
118+
NotAfter: notAfter,
106119
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
107120
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
108121
BasicConstraintsValid: true,
@@ -132,3 +145,51 @@ func CreateSignedServingPair(expiresIn int, ca *KeyPair, hosts []string) (*KeyPa
132145

133146
return servingCert, nil
134147
}
148+
149+
// PEMToCert converts the PEM block of the given byte array to an x509 certificate
150+
func PEMToCert(certPEM []byte) (*x509.Certificate, error) {
151+
block, _ := pem.Decode(certPEM)
152+
if block == nil {
153+
return nil, fmt.Errorf("cert PEM empty")
154+
}
155+
156+
cert, err := x509.ParseCertificate(block.Bytes)
157+
if err != nil {
158+
return nil, err
159+
}
160+
161+
return cert, nil
162+
}
163+
164+
// VerifyCert checks that the given cert is signed and trusted by the given CA
165+
func VerifyCert(ca, cert *x509.Certificate, host string) error {
166+
roots := x509.NewCertPool()
167+
roots.AddCert(ca)
168+
169+
opts := x509.VerifyOptions{
170+
DNSName: host,
171+
Roots: roots,
172+
}
173+
174+
if _, err := cert.Verify(opts); err != nil {
175+
return err
176+
}
177+
178+
return nil
179+
}
180+
181+
// Active checks if the given cert is within its valid time window
182+
func Active(cert *x509.Certificate) bool {
183+
now := time.Now()
184+
active := now.After(cert.NotBefore) && now.Before(cert.NotAfter)
185+
return active
186+
}
187+
188+
type PEMHash func(certPEM []byte) (hash string)
189+
190+
func PEMSHA256(certPEM []byte) (hash string) {
191+
hasher := sha256.New()
192+
hasher.Write(certPEM)
193+
hash = hex.EncodeToString(hasher.Sum(nil))
194+
return
195+
}

0 commit comments

Comments
 (0)