Skip to content

Commit 778391d

Browse files
Add service UID as x509 extension to service server certs
ElasticSearch SearchGuard has a requirement that an attribute of the certificate be used to identify intracluster traffic. That value is currently a registeredID as a SubjectAlternativeName, which is questionable. This PR includes the service UID as an x509 extension (using the new OpenShift OID arc we have been allocated) in order to enable a caller to have an unforgeable value on the certificate they can consult to identifier other cluster members. This also adds more traceability to the certificate for future use.
1 parent 15872c0 commit 778391d

File tree

8 files changed

+178
-20
lines changed

8 files changed

+178
-20
lines changed

pkg/cmd/server/crypto/crypto.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,9 +371,18 @@ func (ca *CA) MakeAndWriteServerCert(certFile, keyFile string, hostnames sets.St
371371
return server, nil
372372
}
373373

374-
func (ca *CA) MakeServerCert(hostnames sets.String, expireDays int) (*TLSCertificateConfig, error) {
374+
// CertificateExtensionFunc is passed a certificate that it may extend, or return an error
375+
// if the extension attempt failed.
376+
type CertificateExtensionFunc func(*x509.Certificate) error
377+
378+
func (ca *CA) MakeServerCert(hostnames sets.String, expireDays int, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) {
375379
serverPublicKey, serverPrivateKey, _ := NewKeyPair()
376380
serverTemplate := newServerCertificateTemplate(pkix.Name{CommonName: hostnames.List()[0]}, hostnames.List(), expireDays, time.Now)
381+
for _, fn := range fns {
382+
if err := fn(serverTemplate); err != nil {
383+
return nil, err
384+
}
385+
}
377386
serverCrt, err := ca.signCertificate(serverTemplate, serverPublicKey)
378387
if err != nil {
379388
return nil, err
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Package extensions defines cryptographic extensions for OpenShift. This
2+
// package contains x509 extension object identifier constants and helpers
3+
// for generating certificates on an OpenShift cluster.
4+
package extensions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package extensions
2+
3+
import (
4+
"encoding/asn1"
5+
)
6+
7+
// oid is a helper function for concatenating OIDs
8+
func oid(o asn1.ObjectIdentifier, extra ...int) asn1.ObjectIdentifier {
9+
return asn1.ObjectIdentifier(append(append([]int{}, o...), extra...))
10+
}
11+
12+
var (
13+
// RedHatOID is the IANA assigned ObjectIdentifier for Red Hat Inc.
14+
RedHatOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 2312}
15+
// OpenShiftOID is the Red Hat assigned OID arc for OpenShift.
16+
OpenShiftOID = oid(RedHatOID, 17)
17+
)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package extensions
2+
3+
import (
4+
"crypto/x509"
5+
"crypto/x509/pkix"
6+
"encoding/asn1"
7+
8+
kapi "k8s.io/kubernetes/pkg/api"
9+
10+
"github.com/openshift/origin/pkg/cmd/server/crypto"
11+
)
12+
13+
var (
14+
// OpenShiftServerSigningOID is the OpenShift assigned OID arc for certificates signed by the OpenShift server.
15+
OpenShiftServerSigningOID = oid(OpenShiftOID, 100)
16+
// OpenShiftServerSigningServiceOID describes the IANA arc for extensions to server certificates generated by the
17+
// OpenShift service signing mechanism. All elements in this arc should only be used when signing server certificates
18+
// for use under a service.
19+
OpenShiftServerSigningServiceOID = oid(OpenShiftServerSigningOID, 2)
20+
// OpenShiftServerSigningServiceUIDOID is an x509 extension that is applied to server certificates generated for services
21+
// representing the UID of the service this certificate was generated for. This value is not guaranteed to match the
22+
// current service UID if the certificates are in the process of being rotated out. The value MUST be an ASN.1
23+
// PrintableString or UTF8String.
24+
OpenShiftServerSigningServiceUIDOID = oid(OpenShiftServerSigningServiceOID, 1)
25+
)
26+
27+
// ServiceServerCertificateExtension returns a CertificateExtensionFunc that will add the
28+
// service UID as an x509 v3 extension to the server certificate.
29+
func ServiceServerCertificateExtension(svc *kapi.Service) crypto.CertificateExtensionFunc {
30+
return func(cert *x509.Certificate) error {
31+
uid, err := asn1.Marshal(svc.UID)
32+
if err != nil {
33+
return err
34+
}
35+
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
36+
Id: OpenShiftServerSigningServiceUIDOID,
37+
Critical: false,
38+
Value: uid,
39+
})
40+
return nil
41+
}
42+
}

pkg/service/controller/servingcert/secret_creating_controller.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"k8s.io/kubernetes/pkg/watch"
2323

2424
"github.com/openshift/origin/pkg/cmd/server/crypto"
25+
"github.com/openshift/origin/pkg/cmd/server/crypto/extensions"
2526
)
2627

2728
const (
@@ -197,7 +198,11 @@ func (sc *ServiceServingCertController) syncService(key string) error {
197198
dnsName := service.Name + "." + service.Namespace + ".svc"
198199
fqDNSName := dnsName + "." + sc.dnsSuffix
199200
certificateLifetime := 365 * 2 // 2 years
200-
servingCert, err := sc.ca.MakeServerCert(sets.NewString(dnsName, fqDNSName), certificateLifetime)
201+
servingCert, err := sc.ca.MakeServerCert(
202+
sets.NewString(dnsName, fqDNSName),
203+
certificateLifetime,
204+
extensions.ServiceServerCertificateExtension(service),
205+
)
201206
if err != nil {
202207
return err
203208
}

pkg/service/controller/servingcert/secret_creating_controller_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package servingcert
22

33
import (
4+
"crypto/x509"
5+
"encoding/asn1"
6+
"encoding/pem"
47
"fmt"
58
"io/ioutil"
69
"reflect"
@@ -16,6 +19,7 @@ import (
1619
"k8s.io/kubernetes/pkg/watch"
1720

1821
"github.com/openshift/origin/pkg/cmd/server/admin"
22+
"github.com/openshift/origin/pkg/cmd/server/crypto/extensions"
1923
)
2024

2125
func controllerSetup(startingObjects []runtime.Object, stopChannel chan struct{}, t *testing.T) ( /*caName*/ string, *fake.Clientset, *watch.FakeWatcher, *ServiceServingCertController) {
@@ -51,6 +55,51 @@ func controllerSetup(startingObjects []runtime.Object, stopChannel chan struct{}
5155
return caOptions.Name, kubeclient, fakeWatch, controller
5256
}
5357

58+
func checkGeneratedCertificate(t *testing.T, certData []byte, service *kapi.Service) {
59+
block, _ := pem.Decode(certData)
60+
if block == nil {
61+
t.Errorf("PEM block not found in secret")
62+
return
63+
}
64+
cert, err := x509.ParseCertificate(block.Bytes)
65+
if err != nil {
66+
t.Errorf("expected valid certificate in first position: %v", err)
67+
return
68+
}
69+
70+
if len(cert.DNSNames) != 2 {
71+
t.Errorf("unexpected DNSNames: %v", cert.DNSNames)
72+
}
73+
for _, s := range cert.DNSNames {
74+
switch s {
75+
case fmt.Sprintf("%s.%s.svc", service.Name, service.Namespace),
76+
fmt.Sprintf("%s.%s.svc.cluster.local", service.Name, service.Namespace):
77+
default:
78+
t.Errorf("unexpected DNSNames: %v", cert.DNSNames)
79+
}
80+
}
81+
82+
found := true
83+
for _, ext := range cert.Extensions {
84+
if extensions.OpenShiftServerSigningServiceUIDOID.Equal(ext.Id) {
85+
var value string
86+
if _, err := asn1.Unmarshal(ext.Value, &value); err != nil {
87+
t.Errorf("unable to parse certificate extension: %v", ext.Value)
88+
continue
89+
}
90+
if value != string(service.UID) {
91+
t.Errorf("unexpected extension value: %v", value)
92+
continue
93+
}
94+
found = true
95+
break
96+
}
97+
}
98+
if !found {
99+
t.Errorf("unable to find service UID certificate extension in cert: %#v", cert)
100+
}
101+
}
102+
54103
func TestBasicControllerFlow(t *testing.T) {
55104
stopChannel := make(chan struct{})
56105
defer close(stopChannel)
@@ -110,6 +159,8 @@ func TestBasicControllerFlow(t *testing.T) {
110159
t.Errorf("expected %v, got %v", expectedSecretAnnotations, newSecret.Annotations)
111160
continue
112161
}
162+
163+
checkGeneratedCertificate(t, newSecret.Data["tls.crt"], serviceToAdd)
113164
foundSecret = true
114165

115166
case action.Matches("update", "services"):

pkg/service/controller/servingcert/secret_updating_controller.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"k8s.io/kubernetes/pkg/watch"
2020

2121
"github.com/openshift/origin/pkg/cmd/server/crypto"
22+
"github.com/openshift/origin/pkg/cmd/server/crypto/extensions"
2223
)
2324

2425
// ServiceServingCertUpdateController is responsible for synchronizing Service objects stored
@@ -220,7 +221,8 @@ func (sc *ServiceServingCertUpdateController) syncSecret(key string) error {
220221
return nil
221222
}
222223

223-
if !sc.requiresRegeneration(obj.(*kapi.Secret)) {
224+
regenerate, service := sc.requiresRegeneration(obj.(*kapi.Secret))
225+
if !regenerate {
224226
return nil
225227
}
226228

@@ -231,10 +233,14 @@ func (sc *ServiceServingCertUpdateController) syncSecret(key string) error {
231233
}
232234
secret := t.(*kapi.Secret)
233235

234-
dnsName := secret.Annotations[ServiceNameAnnotation] + "." + secret.Namespace + ".svc"
236+
dnsName := service.Name + "." + secret.Namespace + ".svc"
235237
fqDNSName := dnsName + "." + sc.dnsSuffix
236238
certificateLifetime := 365 * 2 // 2 years
237-
servingCert, err := sc.ca.MakeServerCert(sets.NewString(dnsName, fqDNSName), certificateLifetime)
239+
servingCert, err := sc.ca.MakeServerCert(
240+
sets.NewString(dnsName, fqDNSName),
241+
certificateLifetime,
242+
extensions.ServiceServerCertificateExtension(service),
243+
)
238244
if err != nil {
239245
return err
240246
}
@@ -248,39 +254,42 @@ func (sc *ServiceServingCertUpdateController) syncSecret(key string) error {
248254
return err
249255
}
250256

251-
func (sc *ServiceServingCertUpdateController) requiresRegeneration(secret *kapi.Secret) bool {
257+
func (sc *ServiceServingCertUpdateController) requiresRegeneration(secret *kapi.Secret) (bool, *kapi.Service) {
252258
serviceName := secret.Annotations[ServiceNameAnnotation]
253259
if len(serviceName) == 0 {
254-
return false
260+
return false, nil
255261
}
256262

257263
serviceObj, exists, err := sc.serviceCache.GetByKey(secret.Namespace + "/" + serviceName)
258264
if err != nil {
259-
return false
265+
return false, nil
260266
}
261267
if !exists {
262-
return false
268+
return false, nil
263269
}
264270

265271
service := serviceObj.(*kapi.Service)
272+
if service.Annotations[ServingCertSecretAnnotation] != secret.Name {
273+
return false, nil
274+
}
266275
if secret.Annotations[ServiceUIDAnnotation] != string(service.UID) {
267-
return false
276+
return false, nil
268277
}
269278

270279
// if we don't have the annotation for expiry, just go ahead and regenerate. It's easier than writing a
271280
// secondary logic flow that creates the expiry dates
272281
expiryString, ok := secret.Annotations[ServingCertExpiryAnnotation]
273282
if !ok {
274-
return true
283+
return true, service
275284
}
276285
expiry, err := time.Parse(time.RFC3339, expiryString)
277286
if err != nil {
278-
return true
287+
return true, service
279288
}
280289

281290
if time.Now().Add(sc.minTimeLeftForCert).After(expiry) {
282-
return true
291+
return true, service
283292
}
284293

285-
return false
294+
return false, nil
286295
}

pkg/service/controller/servingcert/secret_updating_controller_test.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,25 @@ func TestRequiresRegenerationServiceUIDMismatch(t *testing.T) {
4545
name: "service-uid-mismatch",
4646
primeServices: func(serviceCache cache.Store) {
4747
serviceCache.Add(&kapi.Service{
48-
ObjectMeta: kapi.ObjectMeta{Namespace: "ns1", Name: "foo", UID: types.UID("uid-2")},
48+
ObjectMeta: kapi.ObjectMeta{Namespace: "ns1", Name: "foo", UID: types.UID("uid-2"), Annotations: map[string]string{ServingCertSecretAnnotation: "mysecret"}},
49+
})
50+
},
51+
secret: &kapi.Secret{
52+
ObjectMeta: kapi.ObjectMeta{
53+
Namespace: "ns1", Name: "mysecret",
54+
Annotations: map[string]string{
55+
ServiceNameAnnotation: "foo",
56+
ServiceUIDAnnotation: "uid-1",
57+
},
58+
},
59+
},
60+
expected: false,
61+
},
62+
{
63+
name: "service secret name mismatch",
64+
primeServices: func(serviceCache cache.Store) {
65+
serviceCache.Add(&kapi.Service{
66+
ObjectMeta: kapi.ObjectMeta{Namespace: "ns1", Name: "foo", UID: types.UID("uid-1"), Annotations: map[string]string{ServingCertSecretAnnotation: "mysecret2"}},
4967
})
5068
},
5169
secret: &kapi.Secret{
@@ -63,7 +81,7 @@ func TestRequiresRegenerationServiceUIDMismatch(t *testing.T) {
6381
name: "no expiry",
6482
primeServices: func(serviceCache cache.Store) {
6583
serviceCache.Add(&kapi.Service{
66-
ObjectMeta: kapi.ObjectMeta{Namespace: "ns1", Name: "foo", UID: types.UID("uid-1")},
84+
ObjectMeta: kapi.ObjectMeta{Namespace: "ns1", Name: "foo", UID: types.UID("uid-1"), Annotations: map[string]string{ServingCertSecretAnnotation: "mysecret"}},
6785
})
6886
},
6987
secret: &kapi.Secret{
@@ -81,7 +99,7 @@ func TestRequiresRegenerationServiceUIDMismatch(t *testing.T) {
8199
name: "bad expiry",
82100
primeServices: func(serviceCache cache.Store) {
83101
serviceCache.Add(&kapi.Service{
84-
ObjectMeta: kapi.ObjectMeta{Namespace: "ns1", Name: "foo", UID: types.UID("uid-1")},
102+
ObjectMeta: kapi.ObjectMeta{Namespace: "ns1", Name: "foo", UID: types.UID("uid-1"), Annotations: map[string]string{ServingCertSecretAnnotation: "mysecret"}},
85103
})
86104
},
87105
secret: &kapi.Secret{
@@ -100,7 +118,7 @@ func TestRequiresRegenerationServiceUIDMismatch(t *testing.T) {
100118
name: "expired expiry",
101119
primeServices: func(serviceCache cache.Store) {
102120
serviceCache.Add(&kapi.Service{
103-
ObjectMeta: kapi.ObjectMeta{Namespace: "ns1", Name: "foo", UID: types.UID("uid-1")},
121+
ObjectMeta: kapi.ObjectMeta{Namespace: "ns1", Name: "foo", UID: types.UID("uid-1"), Annotations: map[string]string{ServingCertSecretAnnotation: "mysecret"}},
104122
})
105123
},
106124
secret: &kapi.Secret{
@@ -119,7 +137,7 @@ func TestRequiresRegenerationServiceUIDMismatch(t *testing.T) {
119137
name: "distant expiry",
120138
primeServices: func(serviceCache cache.Store) {
121139
serviceCache.Add(&kapi.Service{
122-
ObjectMeta: kapi.ObjectMeta{Namespace: "ns1", Name: "foo", UID: types.UID("uid-1")},
140+
ObjectMeta: kapi.ObjectMeta{Namespace: "ns1", Name: "foo", UID: types.UID("uid-1"), Annotations: map[string]string{ServingCertSecretAnnotation: "mysecret"}},
123141
})
124142
},
125143
secret: &kapi.Secret{
@@ -140,9 +158,12 @@ func TestRequiresRegenerationServiceUIDMismatch(t *testing.T) {
140158
serviceCache: cache.NewStore(framework.DeletionHandlingMetaNamespaceKeyFunc),
141159
}
142160
tc.primeServices(c.serviceCache)
143-
actual := c.requiresRegeneration(tc.secret)
161+
actual, service := c.requiresRegeneration(tc.secret)
144162
if tc.expected != actual {
145163
t.Errorf("%s: expected %v, got %v", tc.name, tc.expected, actual)
146164
}
165+
if service == nil && tc.expected {
166+
t.Errorf("%s: should have returned service", tc.name)
167+
}
147168
}
148169
}

0 commit comments

Comments
 (0)