Skip to content
This repository was archived by the owner on Jul 30, 2021. It is now read-only.

Commit 074c001

Browse files
authored
Merge pull request #260 from chuckha/certs
✨ Adds support for external etcd & respects custom certificates directory from kubeadm
2 parents 54a04d8 + 56fcbc0 commit 074c001

File tree

8 files changed

+245
-71
lines changed

8 files changed

+245
-71
lines changed

cloudinit/cloudinit_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestNewInitControlPlaneAdditionalFileEncodings(t *testing.T) {
4646
Users: nil,
4747
NTP: nil,
4848
},
49-
Certificates: cluster.NewCertificates(),
49+
Certificates: cluster.Certificates{},
5050
ClusterConfiguration: "my-cluster-config",
5151
InitConfiguration: "my-init-config",
5252
}

cloudinit/controlplane_init.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,6 @@ type ControlPlaneInput struct {
5252
// NewInitControlPlane returns the user data string to be used on a controlplane instance.
5353
func NewInitControlPlane(input *ControlPlaneInput) ([]byte, error) {
5454
input.Header = cloudConfigHeader
55-
if err := input.Certificates.EnsureAllExist(); err != nil {
56-
return nil, err
57-
}
58-
5955
input.WriteFiles = input.Certificates.AsFiles()
6056
input.WriteFiles = append(input.WriteFiles, input.AdditionalFiles...)
6157
userData, err := generate("InitControlplane", controlPlaneCloudInit, input)

cloudinit/controlplane_join.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,7 @@ type ControlPlaneJoinInput struct {
5050
// NewJoinControlPlane returns the user data string to be used on a new control plane instance.
5151
func NewJoinControlPlane(input *ControlPlaneJoinInput) ([]byte, error) {
5252
input.Header = cloudConfigHeader
53-
if err := input.Certificates.EnsureAllExist(); err != nil {
54-
return nil, err
55-
}
56-
53+
// TODO: Consider validating that the correct certificates exist. It is different for external/stacked etcd
5754
input.WriteFiles = input.Certificates.AsFiles()
5855
input.WriteFiles = append(input.WriteFiles, input.AdditionalFiles...)
5956
userData, err := generate("JoinControlplane", controlPlaneJoinCloudInit, input)

controllers/kubeadmconfig_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
227227
return ctrl.Result{}, err
228228
}
229229

230-
certificates := internalcluster.NewCertificates()
230+
certificates := internalcluster.NewCertificatesForControlPlane(config.Spec.ClusterConfiguration)
231231
if err := certificates.LookupOrGenerate(ctx, r.Client, cluster, config); err != nil {
232232
log.Error(err, "unable to lookup or create cluster certificates")
233233
return ctrl.Result{}, err
@@ -271,7 +271,7 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
271271
}
272272
}
273273

274-
certificates := internalcluster.NewCertificates()
274+
certificates := internalcluster.NewCertificatesForWorker(config.Spec.JoinConfiguration.CACertPath)
275275
if err := certificates.Lookup(ctx, r.Client, cluster); err != nil {
276276
log.Error(err, "unable to lookup cluster certificates")
277277
return ctrl.Result{}, err

controllers/kubeadmconfig_controller_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1267,7 +1267,10 @@ func newControlPlaneInitKubeadmConfig(machine *clusterv1.Machine, name string) *
12671267

12681268
func createSecrets(t *testing.T, cluster *clusterv1.Cluster, owner *bootstrapv1.KubeadmConfig) []runtime.Object {
12691269
out := []runtime.Object{}
1270-
certificates := internalcluster.NewCertificates()
1270+
if owner.Spec.ClusterConfiguration == nil {
1271+
owner.Spec.ClusterConfiguration = &kubeadmv1beta1.ClusterConfiguration{}
1272+
}
1273+
certificates := internalcluster.NewCertificatesForControlPlane(owner.Spec.ClusterConfiguration)
12711274
if err := certificates.Generate(); err != nil {
12721275
t.Fatal(err)
12731276
}

docs/external-etcd.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Support for external etcd
2+
3+
Cluster API Bootstrap Provider Kubeadm supports using an external etcd cluster for your workload Kubernetes clusters.
4+
5+
### ⚠️ Warnings ⚠️
6+
7+
Before getting started you should be aware of the expectations that come with using an external etcd cluster.
8+
9+
* Cluster API is unable to manage any aspect of the external etcd cluster.
10+
* Depending on how you configure your etcd nodes you may incur additional cloud costs in data transfer.
11+
* As an example, cross availability zone traffic can cost money on cloud providers. You don't have to deploy etcd
12+
across availability zones, but if you do please be aware of the costs.
13+
14+
### Getting started
15+
16+
To use this, you will need to create an etcd cluster and generate an apiserver-etcd-client key/pair.
17+
[`etcdadm`](https://github.com/kubernetes-sigs/etcdadm) is a good way to get started if you'd like to test this
18+
behavior.
19+
20+
Once you create an etcd cluster, you will want to base64 encode the `/etc/etcd/pki/apiserver-etcd-client.crt`,
21+
`/etc/etcd/pki/apiserver-etcd-client.key`, and `/etc/etcd/pki/server.crt` files and put them in two secrets. The secrets
22+
must be formatted as follows and the cert material must be base64 encoded:
23+
24+
```yaml
25+
# Kubernetes APIServer etcd client certificate
26+
kind: Secret
27+
apiVersion: v1
28+
metadata:
29+
name: $CLUSTER_NAME-apiserver-etcd-client
30+
namespace: $CLUSTER_NAMESPACE
31+
data:
32+
tls.crt: |
33+
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCRENDQWV5Z0F3SUJBZ0lJZFlkclZUMzV0
34+
NW93RFFZSktvWklodmNOQVFFTEJRQXdEekVOTUFzR0ExVUUKQXhNRVpYUmpaREFlRncweE9UQTVN
35+
...
36+
tls.key: |
37+
LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBdlFlTzVKOE5j
38+
VCtDeGRubFR3alpuQ3YwRzByY0tETklhZzlSdFdrZ1p4MEcxVm1yClA4Zy9BRkhXVHdxSTUrNi81
39+
...
40+
```
41+
42+
```yaml
43+
# Etcd's CA crt file to validate the generated client certificates
44+
kind: Secret
45+
apiVersion: v1
46+
metadata:
47+
name: $CLUSTER_NAME-etcd
48+
namespace: $CLUSTER_NAMESPACE
49+
data:
50+
tls.crt: |
51+
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURBRENDQWVpZ0F3SUJBZ0lJRDNrVVczaDIy
52+
K013RFFZSktvWklodmNOQVFFTEJRQXdEekVOTUFzR0ExVUUKQXhNRVpYUmpaREFlRncweE9UQTVN
53+
...
54+
```
55+
56+
After that the rest is standard Kubeadm. Config your ClusterConfiguration as follows:
57+
58+
```yaml
59+
apiVersion: bootstrap.cluster.x-k8s.io/v1alpha2
60+
kind: KubeadmConfig
61+
metadata:
62+
name: CLUSTER_NAME-controlplane-0
63+
namespace: CLUSTER_NAMESPACE
64+
spec:
65+
... # initConfiguration goes here
66+
clusterConfiguration:
67+
etcd:
68+
external:
69+
endpoints:
70+
- https://10.0.0.230:2379
71+
caFile: /etc/kubernetes/pki/etcd/ca.crt
72+
certFile: /etc/kubernetes/pki/apiserver-etcd-client.crt
73+
keyFile: /etc/kubernetes/pki/apiserver-etcd-client.key
74+
... # other clusterConfiguration goes here
75+
```
76+
77+
Create your cluster as normal!

internal/cluster/certificates.go

Lines changed: 116 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"crypto/x509/pkix"
2626
"encoding/hex"
2727
"math/big"
28+
"path/filepath"
2829
"strings"
2930
"time"
3031

@@ -34,6 +35,7 @@ import (
3435
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3536
"k8s.io/client-go/util/cert"
3637
bootstrapv1 "sigs.k8s.io/cluster-api-bootstrap-provider-kubeadm/api/v1alpha2"
38+
"sigs.k8s.io/cluster-api-bootstrap-provider-kubeadm/kubeadm/v1beta1"
3739
clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha2"
3840
"sigs.k8s.io/cluster-api/util/certs"
3941
"sigs.k8s.io/cluster-api/util/secret"
@@ -51,6 +53,11 @@ const (
5153

5254
// FrontProxyCA is the secret name suffix for Front Proxy CA
5355
FrontProxyCA secret.Purpose = "proxy"
56+
57+
// APIServerEtcdClient is the secret name of user-supplied secret containing the apiserver-etcd-client key/cert
58+
APIServerEtcdClient secret.Purpose = "apiserver-etcd-client"
59+
60+
defaultCertificatesDir = "/etc/kubernetes/pki"
5461
)
5562

5663
var (
@@ -67,13 +74,65 @@ var (
6774
// Certificates are the certificates necessary to bootstrap a cluster.
6875
type Certificates []*Certificate
6976

70-
// NewCertificates return an initialized but empty set of CA certificates needed to bootstrap a cluster.
71-
func NewCertificates() Certificates {
77+
// NewCertificatesForControlPlane returns a list of certificates configured for a control plane node
78+
func NewCertificatesForControlPlane(config *v1beta1.ClusterConfiguration) Certificates {
79+
if config.CertificatesDir == "" {
80+
config.CertificatesDir = defaultCertificatesDir
81+
}
82+
83+
certificates := Certificates{
84+
&Certificate{
85+
Purpose: secret.ClusterCA,
86+
CertFile: filepath.Join(config.CertificatesDir, "ca.crt"),
87+
KeyFile: filepath.Join(config.CertificatesDir, "ca.key"),
88+
},
89+
&Certificate{
90+
Purpose: ServiceAccount,
91+
CertFile: filepath.Join(config.CertificatesDir, "sa.pub"),
92+
KeyFile: filepath.Join(config.CertificatesDir, "sa.key"),
93+
},
94+
&Certificate{
95+
Purpose: FrontProxyCA,
96+
CertFile: filepath.Join(config.CertificatesDir, "front-proxy-ca.crt"),
97+
KeyFile: filepath.Join(config.CertificatesDir, "front-proxy-ca.key"),
98+
},
99+
}
100+
101+
etcdCert := &Certificate{
102+
Purpose: EtcdCA,
103+
CertFile: filepath.Join(config.CertificatesDir, "etcd", "ca.crt"),
104+
KeyFile: filepath.Join(config.CertificatesDir, "etcd", "ca.key"),
105+
}
106+
107+
// TODO make sure all the fields are actually defined and return an error if not
108+
if config.Etcd.External != nil {
109+
etcdCert = &Certificate{
110+
Purpose: EtcdCA,
111+
CertFile: config.Etcd.External.CAFile,
112+
}
113+
apiserverEtcdClientCert := &Certificate{
114+
Purpose: APIServerEtcdClient,
115+
CertFile: config.Etcd.External.CertFile,
116+
KeyFile: config.Etcd.External.KeyFile,
117+
}
118+
certificates = append(certificates, apiserverEtcdClientCert)
119+
}
120+
121+
certificates = append(certificates, etcdCert)
122+
return certificates
123+
}
124+
125+
// NewCertificatesForWorker return an initialized but empty set of CA certificates needed to bootstrap a cluster.
126+
func NewCertificatesForWorker(caCertPath string) Certificates {
127+
if caCertPath == "" {
128+
caCertPath = filepath.Join(defaultCertificatesDir, "ca.crt")
129+
}
130+
72131
return Certificates{
73-
&Certificate{Purpose: secret.ClusterCA},
74-
&Certificate{Purpose: EtcdCA},
75-
&Certificate{Purpose: ServiceAccount},
76-
&Certificate{Purpose: FrontProxyCA},
132+
&Certificate{
133+
Purpose: secret.ClusterCA,
134+
CertFile: caCertPath,
135+
},
77136
}
78137
}
79138

@@ -138,6 +197,8 @@ func (c Certificates) Generate() error {
138197
if certificate.KeyPair == nil {
139198
var generator certGenerator
140199
switch certificate.Purpose {
200+
case APIServerEtcdClient: // Do not generate the APIServerEtcdClient key pair. It is user supplied
201+
continue
141202
case ServiceAccount:
142203
generator = generateServiceAccountKeys
143204
default:
@@ -191,9 +252,10 @@ func (c Certificates) LookupOrGenerate(ctx context.Context, ctrlclient client.Cl
191252

192253
// Certificate represents a single certificate CA.
193254
type Certificate struct {
194-
Generated bool
195-
Purpose secret.Purpose
196-
KeyPair *certs.KeyPair
255+
Generated bool
256+
Purpose secret.Purpose
257+
KeyPair *certs.KeyPair
258+
CertFile, KeyFile string
197259
}
198260

199261
// Hashes hashes all the certificates stored in a CA certificate.
@@ -244,63 +306,56 @@ func (c *Certificate) AsSecret(cluster *clusterv1.Cluster, config *bootstrapv1.K
244306
return s
245307
}
246308

309+
// AsFiles converts the certificate to a slice of Files that may have 0, 1 or 2 Files.
310+
func (c *Certificate) AsFiles() []bootstrapv1.File {
311+
out := make([]bootstrapv1.File, 0)
312+
if len(c.KeyPair.Cert) > 0 {
313+
out = append(out, bootstrapv1.File{
314+
Path: c.CertFile,
315+
Owner: rootOwnerValue,
316+
Permissions: "0640",
317+
Content: string(c.KeyPair.Cert),
318+
})
319+
}
320+
if len(c.KeyPair.Key) > 0 {
321+
out = append(out, bootstrapv1.File{
322+
Path: c.KeyFile,
323+
Owner: rootOwnerValue,
324+
Permissions: "0600",
325+
Content: string(c.KeyPair.Key),
326+
})
327+
}
328+
return out
329+
}
330+
247331
// AsFiles converts a slice of certificates into bootstrap files.
248332
func (c Certificates) AsFiles() []bootstrapv1.File {
249333
clusterCA := c.GetByPurpose(secret.ClusterCA)
250334
etcdCA := c.GetByPurpose(EtcdCA)
251335
frontProxyCA := c.GetByPurpose(FrontProxyCA)
252336
serviceAccountKey := c.GetByPurpose(ServiceAccount)
253337

254-
return []bootstrapv1.File{
255-
{
256-
Path: "/etc/kubernetes/pki/ca.crt",
257-
Owner: rootOwnerValue,
258-
Permissions: "0640",
259-
Content: string(clusterCA.KeyPair.Cert),
260-
},
261-
{
262-
Path: "/etc/kubernetes/pki/ca.key",
263-
Owner: rootOwnerValue,
264-
Permissions: "0600",
265-
Content: string(clusterCA.KeyPair.Key),
266-
},
267-
{
268-
Path: "/etc/kubernetes/pki/etcd/ca.crt",
269-
Owner: rootOwnerValue,
270-
Permissions: "0640",
271-
Content: string(etcdCA.KeyPair.Cert),
272-
},
273-
{
274-
Path: "/etc/kubernetes/pki/etcd/ca.key",
275-
Owner: rootOwnerValue,
276-
Permissions: "0600",
277-
Content: string(etcdCA.KeyPair.Key),
278-
},
279-
{
280-
Path: "/etc/kubernetes/pki/front-proxy-ca.crt",
281-
Owner: rootOwnerValue,
282-
Permissions: "0640",
283-
Content: string(frontProxyCA.KeyPair.Cert),
284-
},
285-
{
286-
Path: "/etc/kubernetes/pki/front-proxy-ca.key",
287-
Owner: rootOwnerValue,
288-
Permissions: "0600",
289-
Content: string(frontProxyCA.KeyPair.Key),
290-
},
291-
{
292-
Path: "/etc/kubernetes/pki/sa.pub",
293-
Owner: rootOwnerValue,
294-
Permissions: "0640",
295-
Content: string(serviceAccountKey.KeyPair.Cert),
296-
},
297-
{
298-
Path: "/etc/kubernetes/pki/sa.key",
299-
Owner: rootOwnerValue,
300-
Permissions: "0600",
301-
Content: string(serviceAccountKey.KeyPair.Key),
302-
},
338+
certFiles := make([]bootstrapv1.File, 0)
339+
if clusterCA != nil {
340+
certFiles = append(certFiles, clusterCA.AsFiles()...)
341+
}
342+
if etcdCA != nil {
343+
certFiles = append(certFiles, etcdCA.AsFiles()...)
344+
}
345+
if frontProxyCA != nil {
346+
certFiles = append(certFiles, frontProxyCA.AsFiles()...)
347+
}
348+
if serviceAccountKey != nil {
349+
certFiles = append(certFiles, serviceAccountKey.AsFiles()...)
303350
}
351+
352+
// these will only exist if external etcd was defined and supplied by the user
353+
apiserverEtcdClientCert := c.GetByPurpose(APIServerEtcdClient)
354+
if apiserverEtcdClientCert != nil {
355+
certFiles = append(certFiles, apiserverEtcdClientCert.AsFiles()...)
356+
}
357+
358+
return certFiles
304359
}
305360

306361
func secretToKeyPair(s *corev1.Secret) (*certs.KeyPair, error) {
@@ -309,9 +364,11 @@ func secretToKeyPair(s *corev1.Secret) (*certs.KeyPair, error) {
309364
return nil, errors.Errorf("missing data for key %s", secret.TLSCrtDataName)
310365
}
311366

367+
// In some cases (external etcd) it's ok if the etcd.key does not exist.
368+
// TODO: some other function should ensure that the certificates we need exist.
312369
key, exists := s.Data[secret.TLSKeyDataName]
313370
if !exists {
314-
return nil, errors.Errorf("missing data for key %s", secret.TLSKeyDataName)
371+
key = []byte("")
315372
}
316373

317374
return &certs.KeyPair{

0 commit comments

Comments
 (0)