Skip to content

Commit d7ee481

Browse files
Merge pull request #525 from njhale/refresh-certs
feat(csv): add cert rotation for owned APIServices
2 parents b8f267d + 98964b3 commit d7ee481

28 files changed

+1601
-241
lines changed

.gitlab-ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ e2e-teardown:
225225
script:
226226
- echo $CD_KUBECONFIG | base64 -d > kubeconfig
227227
- export KUBECONFIG=./kubeconfig
228+
- kubectl delete apiservice v1alpha1.packages.apps.redhat.com --ignore-not-found=true
228229
- kubectl delete ns --ignore-not-found=true e2e-${CI_COMMIT_REF_SLUG}-${SHA8}
229230
- kubectl get pods -o wide -n e2e-${CI_COMMIT_REF_SLUG}-${SHA8}
230231
stage: test_teardown
@@ -284,6 +285,7 @@ stop-preview:
284285
script:
285286
- echo $CD_KUBECONFIG | base64 -d > kubeconfig
286287
- export KUBECONFIG=./kubeconfig
288+
- kubectl delete apiservice v1alpha1.packages.apps.redhat.com --ignore-not-found=true
287289
- kubectl delete ns --ignore-not-found=true ci-olm-${CI_COMMIT_REF_SLUG}
288290
- kubectl get pods -o wide -n ci-olm-${CI_COMMIT_REF_SLUG}
289291
stage: deploy_preview

.gitlab-ci/base_jobs.libsonnet

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ local appr = utils.appr;
136136
before_script: [],
137137
script:
138138
k8s.setKubeConfig(self.localvars.kubeconfig) + [
139+
"kubectl delete apiservice v1alpha1.packages.apps.redhat.com --ignore-not-found=true",
139140
"kubectl delete ns --ignore-not-found=true %s" % self.localvars.namespace,
140141
"kubectl get pods -o wide -n %s" % self.localvars.namespace,
141142
],

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ spec:
180180
values:
181181
type: array
182182
description: set of values for the expression
183+
183184
apiservicedefinitions:
184185
type: object
185186
properties:

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

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,18 +181,20 @@ const (
181181
type ConditionReason string
182182

183183
const (
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"
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"
196198
)
197199

198200
// Conditions appear in the status as a record of state transitions on the ClusterServiceVersion
@@ -225,7 +227,7 @@ func (csv ClusterServiceVersion) OwnsCRD(name string) bool {
225227
return false
226228
}
227229

228-
// ConditionReason is a camelcased reason for the status of a RequirementStatus or DependentStatus
230+
// StatusReason is a camelcased reason for the status of a RequirementStatus or DependentStatus
229231
type StatusReason string
230232

231233
const (
@@ -278,11 +280,17 @@ type ClusterServiceVersionStatus struct {
278280
Conditions []ClusterServiceVersionCondition `json:"conditions,omitempty"`
279281
// The status of each requirement for this CSV
280282
RequirementStatus []RequirementStatus `json:"requirementStatus,omitempty"`
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
287+
// +optional
288+
CertsRotateAt metav1.Time `json:"certsRotateAt,omitempty"`
281289
}
282290

291+
// ClusterServiceVersion is a Custom Resource of type `ClusterServiceVersionSpec`.
283292
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
284293
// +genclient
285-
// ClusterServiceVersion is a Custom Resource of type `ClusterServiceVersionSpec`.
286294
type ClusterServiceVersion struct {
287295
metav1.TypeMeta `json:",inline"`
288296
metav1.ObjectMeta `json:"metadata"`

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

Lines changed: 2 additions & 0 deletions
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: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +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"
12+
"fmt"
13+
"math"
1014
"math/big"
1115
"time"
1216
)
1317

18+
const (
19+
Organization = "Red Hat, Inc."
20+
)
21+
1422
// KeyPair stores an x509 certificate and its ECDSA private key
1523
type KeyPair struct {
1624
Cert *x509.Certificate
@@ -40,16 +48,25 @@ func (kp *KeyPair) ToPEM() (certPEM []byte, privPEM []byte, err error) {
4048
return
4149
}
4250

43-
func GenerateCA() (*KeyPair, error) {
51+
// GenerateCA generates a self-signed CA cert/key pair that expires in expiresIn days
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())
56+
}
57+
58+
serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64))
59+
if err != nil {
60+
return nil, err
61+
}
62+
4463
caDetails := &x509.Certificate{
45-
//TODO(Nick): figure out what to use for a SerialNumber
46-
SerialNumber: big.NewInt(1653),
64+
SerialNumber: serial,
4765
Subject: pkix.Name{
48-
Organization: []string{"Red Hat, Inc."},
66+
Organization: []string{Organization},
4967
},
50-
NotBefore: time.Now(),
51-
// Valid for 2 years
52-
NotAfter: time.Now().AddDate(2, 0, 0),
68+
NotBefore: notBefore,
69+
NotAfter: notAfter,
5370
IsCA: true,
5471
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
5572
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
@@ -80,16 +97,25 @@ func GenerateCA() (*KeyPair, error) {
8097
return ca, nil
8198
}
8299

83-
func CreateSignedServingPair(ca *KeyPair, hosts []string) (*KeyPair, error) {
100+
// CreateSignedServingPair creates a serving cert/key pair signed by the given ca
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
110+
}
111+
84112
certDetails := &x509.Certificate{
85-
//TODO(Nick): figure out what to use for a SerialNumber
86-
SerialNumber: big.NewInt(1653),
113+
SerialNumber: serial,
87114
Subject: pkix.Name{
88-
Organization: []string{"Red Hat, Inc."},
115+
Organization: []string{Organization},
89116
},
90-
NotBefore: time.Now(),
91-
// Valid for 2 years
92-
NotAfter: time.Now().AddDate(2, 0, 0),
117+
NotBefore: notBefore,
118+
NotAfter: notAfter,
93119
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
94120
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
95121
BasicConstraintsValid: true,
@@ -119,3 +145,51 @@ func CreateSignedServingPair(ca *KeyPair, hosts []string) (*KeyPair, error) {
119145

120146
return servingCert, nil
121147
}
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+
}

pkg/controller/install/rule_checker.go

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import (
55

66
corev1 "k8s.io/api/core/v1"
77
rbacv1 "k8s.io/api/rbac/v1"
8-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
98
"k8s.io/apimachinery/pkg/labels"
109
"k8s.io/apiserver/pkg/authorization/authorizer"
1110
crbacv1 "k8s.io/client-go/listers/rbac/v1"
1211
rbacauthorizer "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"
1312

1413
"github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
14+
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil"
1515
)
1616

1717
// RuleChecker is used to verify whether PolicyRules are satisfied by existing Roles or ClusterRoles
@@ -81,7 +81,7 @@ func (c *CSVRuleChecker) GetRole(namespace, name string) (*rbacv1.Role, error) {
8181
}
8282

8383
// check if the Role has an OwnerConflict with the client's CSV
84-
if role != nil && c.hasOwnerConflicts(role.GetOwnerReferences()) {
84+
if role != nil && ownerutil.HasOwnerConflict(c.csv, role.GetOwnerReferences()) {
8585
return &rbacv1.Role{}, nil
8686
}
8787

@@ -98,7 +98,7 @@ func (c *CSVRuleChecker) ListRoleBindings(namespace string) ([]*rbacv1.RoleBindi
9898
// filter based on OwnerReferences
9999
var filtered []*rbacv1.RoleBinding
100100
for _, rb := range rbList {
101-
if !c.hasOwnerConflicts(rb.GetOwnerReferences()) {
101+
if !ownerutil.HasOwnerConflict(c.csv, rb.GetOwnerReferences()) {
102102
filtered = append(filtered, rb)
103103
}
104104
}
@@ -114,7 +114,7 @@ func (c *CSVRuleChecker) GetClusterRole(name string) (*rbacv1.ClusterRole, error
114114
}
115115

116116
// check if the ClusterRole has an OwnerConflict with the client's CSV
117-
if clusterRole != nil && c.hasOwnerConflicts(clusterRole.GetOwnerReferences()) {
117+
if clusterRole != nil && ownerutil.HasOwnerConflict(c.csv, clusterRole.GetOwnerReferences()) {
118118
return &rbacv1.ClusterRole{}, nil
119119
}
120120

@@ -131,39 +131,15 @@ func (c *CSVRuleChecker) ListClusterRoleBindings() ([]*rbacv1.ClusterRoleBinding
131131
// filter based on OwnerReferences
132132
var filtered []*rbacv1.ClusterRoleBinding
133133
for _, crb := range crbList {
134-
if !c.hasOwnerConflicts(crb.GetOwnerReferences()) {
134+
if !ownerutil.HasOwnerConflict(c.csv, crb.GetOwnerReferences()) {
135135
filtered = append(filtered, crb)
136136
}
137137
}
138138

139139
return filtered, nil
140140
}
141141

142-
// hasOwnerConflicts checks if the given list of OwnerReferences points to CSVs other than the
143-
// CSVRuleChecker's. The method returns true if the list of OwnerReferences contains elements of Kind
144-
// ClusterServiceVersion but does not include an OwnerReference to the CSVRuleChecker's CSV. If there
145-
// are no OwnerReferences of Kind ClusterServiceVersion, or there is but one element is an OwnerReference
146-
// to the CSVRuleChecker's CSV, then the method returns false.
147-
//
148-
// Note: This is imporant when determining if a Role, RoleBinding, ClusterRole, or ClusterRoleBinding
149-
// can be used to satisfy permissions of a CSV. If the CSVRuleChecker's CSV is not a member of the RBAC resource's
150-
// OwnerReferences, then we know the resource can be garbage collected by OLM independently of the CSVRuleChecker's
151-
// CSV
152-
func (c *CSVRuleChecker) hasOwnerConflicts(ownerRefs []metav1.OwnerReference) bool {
153-
conflicts := false
154-
for _, ownerRef := range ownerRefs {
155-
if ownerRef.Kind == v1alpha1.ClusterServiceVersionKind {
156-
if ownerRef.Name == c.csv.GetName() && ownerRef.UID == c.csv.GetUID() {
157-
return false
158-
}
159-
160-
conflicts = true
161-
}
162-
}
163-
164-
return conflicts
165-
}
166-
142+
// ruleValid returns an error if the given PolicyRule is not valid (resource and nonresource attributes defined)
167143
func ruleValid(rule rbacv1.PolicyRule) error {
168144
if len(rule.Verbs) == 0 {
169145
return fmt.Errorf("policy rule must have at least one verb")

0 commit comments

Comments
 (0)