diff --git a/cmd/ctl/create/cluster.go b/cmd/ctl/create/cluster.go index fe9460e6..e4a13de9 100644 --- a/cmd/ctl/create/cluster.go +++ b/cmd/ctl/create/cluster.go @@ -42,6 +42,7 @@ type CreateClusterOptions struct { DownloadCmd string Artifact string SkipInstallPackages bool + CertificatesDir string } func NewCreateClusterOptions() *CreateClusterOptions { @@ -112,6 +113,7 @@ func (o *CreateClusterOptions) Run() error { ContainerManager: o.ContainerManager, Artifact: o.Artifact, SkipInstallPackages: o.SkipInstallPackages, + CertificatesDir: o.CertificatesDir, } return pipelines.CreateCluster(arg, o.DownloadCmd) @@ -125,6 +127,7 @@ func (o *CreateClusterOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVarP(&o.SkipPullImages, "skip-pull-images", "", false, "Skip pre pull images") cmd.Flags().BoolVarP(&o.SkipPushImages, "skip-push-images", "", false, "Skip pre push images") cmd.Flags().StringVarP(&o.ContainerManager, "container-manager", "", "docker", "Container runtime: docker, crio, containerd and isula.") + cmd.Flags().StringVarP(&o.CertificatesDir, "certificates-dir", "", "", "Specifies where to store or look for all required certificates.") cmd.Flags().StringVarP(&o.DownloadCmd, "download-cmd", "", "curl -L -o %s %s", `The user defined command to download the necessary binary files. The first param '%s' is output path, the second param '%s', is the URL`) cmd.Flags().StringVarP(&o.Artifact, "artifact", "a", "", "Path to a KubeKey artifact") diff --git a/go.mod b/go.mod index f4f7b071..b0908378 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( k8s.io/client-go v0.23.1 k8s.io/code-generator v0.23.1 k8s.io/kubectl v0.23.1 + k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b sigs.k8s.io/controller-runtime v0.11.0 ) @@ -190,7 +191,6 @@ require ( k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c // indirect k8s.io/klog/v2 v2.30.0 // indirect k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect - k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect oras.land/oras-go v0.4.0 // indirect sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect sigs.k8s.io/kustomize/api v0.10.1 // indirect diff --git a/pkg/common/kube_runtime.go b/pkg/common/kube_runtime.go index d11fe375..b7c5eb80 100644 --- a/pkg/common/kube_runtime.go +++ b/pkg/common/kube_runtime.go @@ -54,6 +54,7 @@ type Argument struct { KubeConfig string Artifact string SkipInstallPackages bool + CertificatesDir string } func NewKubeRuntime(flag string, arg Argument) (*KubeRuntime, error) { diff --git a/pkg/core/connector/ssh.go b/pkg/core/connector/ssh.go index e4ae64ef..5031b093 100644 --- a/pkg/core/connector/ssh.go +++ b/pkg/core/connector/ssh.go @@ -19,6 +19,7 @@ package connector import ( "bufio" "context" + "encoding/base64" "fmt" "github.com/kubesphere/kubekey/pkg/core/logger" "github.com/kubesphere/kubekey/pkg/core/util" @@ -393,7 +394,9 @@ func (c *connection) Fetch(local, remote string, host Host) error { // return fmt.Errorf("open remote file failed %v, remote path: %s", err, remote) //} //defer srcFile.Close() - output, _, err := c.Exec(SudoPrefix(fmt.Sprintf("cat %s", remote)), host) + + // Base64 encoding is performed on the contents of the file to prevent garbled code in the target file. + output, _, err := c.Exec(SudoPrefix(fmt.Sprintf("cat %s | base64 -w 0", remote)), host) if err != nil { return fmt.Errorf("open remote file failed %v, remote path: %s", err, remote) } @@ -410,8 +413,15 @@ func (c *connection) Fetch(local, remote string, host Host) error { defer dstFile.Close() // copy to local file //_, err = srcFile.WriteTo(dstFile) - _, err = dstFile.WriteString(output) - return err + if base64Str, err := base64.StdEncoding.DecodeString(output); err != nil { + return err + } else { + if _, err = dstFile.WriteString(string(base64Str)); err != nil { + return err + } + } + + return nil } type scpErr struct { diff --git a/pkg/etcd/certs.go b/pkg/etcd/certs.go new file mode 100644 index 00000000..6a689ebd --- /dev/null +++ b/pkg/etcd/certs.go @@ -0,0 +1,208 @@ +/* + Copyright 2021 The KubeSphere Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package etcd + +import ( + "crypto/x509" + "fmt" + kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/apis/kubekey/v1alpha2" + "github.com/kubesphere/kubekey/pkg/common" + "github.com/kubesphere/kubekey/pkg/core/connector" + "github.com/kubesphere/kubekey/pkg/utils/certs" + "github.com/pkg/errors" + "k8s.io/client-go/util/cert" + certutil "k8s.io/client-go/util/cert" + netutils "k8s.io/utils/net" + "net" + "path/filepath" + "strings" +) + +// KubekeyCertEtcdCA is the definition of the root CA used by the hosted etcd server. +func KubekeyCertEtcdCA() *certs.KubekeyCert { + return &certs.KubekeyCert{ + Name: "etcd-ca", + LongName: "self-signed CA to provision identities for etcd", + BaseName: "ca", + Config: certs.CertConfig{ + Config: certutil.Config{ + CommonName: "etcd-ca", + }, + }, + } +} + +// KubekeyCertEtcdAdmin is the definition of the cert for etcd admin. +func KubekeyCertEtcdAdmin(hostname string, altNames *certutil.AltNames) *certs.KubekeyCert { + l := strings.Split(hostname, ".") + return &certs.KubekeyCert{ + Name: "etcd-admin", + LongName: "certificate for etcd admin", + BaseName: fmt.Sprintf("admin-%s", hostname), + CAName: "etcd-ca", + Config: certs.CertConfig{ + Config: certutil.Config{ + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + AltNames: *altNames, + CommonName: fmt.Sprintf("etcd-admin-%s", l[0]), + }, + }, + } +} + +// KubekeyCertEtcdMember is the definition of the cert for etcd member. +func KubekeyCertEtcdMember(hostname string, altNames *certutil.AltNames) *certs.KubekeyCert { + l := strings.Split(hostname, ".") + return &certs.KubekeyCert{ + Name: "etcd-member", + LongName: "certificate for etcd member", + BaseName: fmt.Sprintf("member-%s", hostname), + CAName: "etcd-ca", + Config: certs.CertConfig{ + Config: certutil.Config{ + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + AltNames: *altNames, + CommonName: fmt.Sprintf("etcd-member-%s", l[0]), + }, + }, + } +} + +// KubekeyCertEtcdMember is the definition of the cert for etcd client. +func KubekeyCertEtcdClient(hostname string, altNames *certutil.AltNames) *certs.KubekeyCert { + l := strings.Split(hostname, ".") + return &certs.KubekeyCert{ + Name: "etcd-client", + LongName: "certificate for etcd client", + BaseName: fmt.Sprintf("node-%s", hostname), + CAName: "etcd-ca", + Config: certs.CertConfig{ + Config: certutil.Config{ + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + AltNames: *altNames, + CommonName: fmt.Sprintf("etcd-node-%s", l[0]), + }, + }, + } +} + +type FetchCerts struct { + common.KubeAction +} + +func (f *FetchCerts) Execute(runtime connector.Runtime) error { + src := "/etc/ssl/etcd/ssl" + dst := fmt.Sprintf("%s/pki/etcd", runtime.GetWorkDir()) + + v, ok := f.PipelineCache.Get(common.ETCDCluster) + if !ok { + return errors.New("get etcd status from pipeline cache failed") + } + + c := v.(*EtcdCluster) + + if c.clusterExist { + certs, err := runtime.GetRunner().SudoCmd("ls /etc/ssl/etcd/ssl/ | grep .pem", false) + if err != nil { + return errors.Wrap(err, "failed to find certificate files") + } + + certsList := strings.Split(certs, "\r\n") + if len(certsList) > 0 { + for _, cert := range certsList { + if err := runtime.GetRunner().Fetch(filepath.Join(dst, cert), filepath.Join(src, cert)); err != nil { + return errors.Wrap(err, fmt.Sprintf("Fetch %s failed", filepath.Join(src, cert))) + } + } + } + } + + return nil +} + +type GenerateCerts struct { + common.KubeAction +} + +func (g *GenerateCerts) Execute(runtime connector.Runtime) error { + var pkiPath string + if g.KubeConf.Arg.CertificatesDir == "" { + pkiPath = fmt.Sprintf("%s/pki/etcd", runtime.GetWorkDir()) + } + + var altName cert.AltNames + + dnsList := []string{"localhost", "etcd.kube-system.svc.cluster.local", "etcd.kube-system.svc", "etcd.kube-system", "etcd"} + ipList := []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback} + + if g.KubeConf.Cluster.ControlPlaneEndpoint.Domain == "" { + dnsList = append(dnsList, kubekeyapiv1alpha2.DefaultLBDomain) + } else { + dnsList = append(dnsList, g.KubeConf.Cluster.ControlPlaneEndpoint.Domain) + } + + for _, host := range g.KubeConf.Cluster.Hosts { + dnsList = append(dnsList, host.Name) + internalAddress := netutils.ParseIPSloppy(host.InternalAddress) + if internalAddress != nil { + ipList = append(ipList, internalAddress) + } + } + + altName.DNSNames = dnsList + altName.IPs = ipList + + files := []string{"ca.pem", "ca-key.pem"} + + // CA + certsList := []*certs.KubekeyCert{KubekeyCertEtcdCA()} + + // Certs + for _, host := range runtime.GetAllHosts() { + if host.IsRole(common.ETCD) { + certsList = append(certsList, KubekeyCertEtcdAdmin(host.GetName(), &altName)) + files = append(files, []string{fmt.Sprintf("admin-%s.pem", host.GetName()), fmt.Sprintf("admin-%s-key.pem", host.GetName())}...) + certsList = append(certsList, KubekeyCertEtcdMember(host.GetName(), &altName)) + files = append(files, []string{fmt.Sprintf("member-%s.pem", host.GetName()), fmt.Sprintf("member-%s-key.pem", host.GetName())}...) + } + if host.IsRole(common.Master) { + certsList = append(certsList, KubekeyCertEtcdClient(host.GetName(), &altName)) + files = append(files, []string{fmt.Sprintf("node-%s.pem", host.GetName()), fmt.Sprintf("node-%s-key.pem", host.GetName())}...) + } + } + + var lastCACert *certs.KubekeyCert + for _, c := range certsList { + if c.CAName == "" { + err := certs.GenerateCA(c, pkiPath, g.KubeConf) + if err != nil { + return err + } + lastCACert = c + } else { + err := certs.GenerateCerts(c, lastCACert, pkiPath, g.KubeConf) + if err != nil { + return err + } + } + } + + g.ModuleCache.Set(LocalCertsDir, pkiPath) + g.ModuleCache.Set(CertsFileList, files) + + return nil +} diff --git a/pkg/etcd/module.go b/pkg/etcd/module.go index adff9c38..9ed9e2e9 100644 --- a/pkg/etcd/module.go +++ b/pkg/etcd/module.go @@ -20,9 +20,7 @@ import ( "github.com/kubesphere/kubekey/pkg/common" "github.com/kubesphere/kubekey/pkg/core/action" "github.com/kubesphere/kubekey/pkg/core/task" - "github.com/kubesphere/kubekey/pkg/core/util" "github.com/kubesphere/kubekey/pkg/etcd/templates" - "path/filepath" ) type PreCheckModule struct { @@ -54,56 +52,26 @@ func (c *CertsModule) Init() { c.Name = "CertsModule" c.Desc = "Sign ETCD cluster certs" - generateCertsScript := &task.RemoteTask{ - Name: "GenerateCertsScript", - Desc: "Generate certs script", - Hosts: c.Runtime.GetHostsByRole(common.ETCD), - Prepare: new(FirstETCDNode), - Action: &action.Template{ - Template: templates.EtcdSslScript, - Dst: filepath.Join(common.ETCDCertDir, templates.EtcdSslScript.Name()), - Data: util.Data{ - "Masters": templates.GenerateHosts(c.Runtime.GetHostsByRole(common.ETCD)), - "Hosts": templates.GenerateHosts(c.Runtime.GetHostsByRole(common.Master)), - }, - }, - Parallel: true, - Retry: 1, - } - - dnsList, ipList := templates.DNSAndIp(c.KubeConf) - generateOpenSSLConf := &task.RemoteTask{ - Name: "GenerateOpenSSLConf", - Desc: "Generate OpenSSL config", - Hosts: c.Runtime.GetHostsByRole(common.ETCD), - Prepare: new(FirstETCDNode), - Action: &action.Template{ - Template: templates.ETCDOpenSSLConf, - Dst: filepath.Join(common.ETCDCertDir, templates.ETCDOpenSSLConf.Name()), - Data: util.Data{ - "Dns": dnsList, - "Ips": ipList, - }, - }, - Parallel: true, - Retry: 1, - } - - execCertsScript := &task.RemoteTask{ - Name: "ExecCertsScript", - Desc: "Exec certs script", + // If the etcd cluster already exists, obtain the certificate in use from the etcd node. + fetchCerts := &task.RemoteTask{ + Name: "FetchETCDCerts", + Desc: "Fetcd etcd certs", Hosts: c.Runtime.GetHostsByRole(common.ETCD), Prepare: new(FirstETCDNode), - Action: new(ExecCertsScript), - Parallel: true, - Retry: 1, + Action: new(FetchCerts), + Parallel: false, + } + + generateCerts := &task.LocalTask{ + Name: "GenerateETCDCerts", + Desc: "Generate etcd Certs", + Action: new(GenerateCerts), } syncCertsFile := &task.RemoteTask{ Name: "SyncCertsFile", Desc: "Synchronize certs file", Hosts: c.Runtime.GetHostsByRole(common.ETCD), - Prepare: &FirstETCDNode{Not: true}, Action: new(SyncCertsFile), Parallel: true, Retry: 1, @@ -120,9 +88,8 @@ func (c *CertsModule) Init() { } c.Tasks = []task.Interface{ - generateCertsScript, - generateOpenSSLConf, - execCertsScript, + fetchCerts, + generateCerts, syncCertsFile, syncCertsToMaster, } diff --git a/pkg/etcd/tasks.go b/pkg/etcd/tasks.go index 811c1402..b4bc497f 100644 --- a/pkg/etcd/tasks.go +++ b/pkg/etcd/tasks.go @@ -103,60 +103,6 @@ func (g *GetStatus) Execute(runtime connector.Runtime) error { return nil } -type ExecCertsScript struct { - common.KubeAction -} - -func (e *ExecCertsScript) Execute(runtime connector.Runtime) error { - _, err := runtime.GetRunner().SudoCmd(fmt.Sprintf("chmod +x %s/make-ssl-etcd.sh", common.ETCDCertDir), false) - if err != nil { - return err - } - - cmd := fmt.Sprintf("/bin/bash -x %s/make-ssl-etcd.sh -f %s/openssl.conf -d %s", common.ETCDCertDir, common.ETCDCertDir, common.ETCDCertDir) - if _, err := runtime.GetRunner().SudoCmd(cmd, false); err != nil { - return errors.Wrap(errors.WithStack(err), "generate etcd certs failed") - } - - tmpCertsDir := filepath.Join(common.TmpDir, "ETCD_certs") - if _, err := runtime.GetRunner().SudoCmd(fmt.Sprintf("cp -r %s %s", common.ETCDCertDir, tmpCertsDir), false); err != nil { - return errors.Wrap(errors.WithStack(err), "copy certs result failed") - } - - localCertsDir := filepath.Join(runtime.GetWorkDir(), "ETCD_certs") - if err := util.CreateDir(localCertsDir); err != nil { - return err - } - - files := generateCertsFiles(runtime) - for _, fileName := range files { - if err := runtime.GetRunner().Fetch(filepath.Join(localCertsDir, fileName), filepath.Join(tmpCertsDir, fileName)); err != nil { - return errors.Wrap(errors.WithStack(err), "fetch etcd certs file failed") - } - } - - e.ModuleCache.Set(LocalCertsDir, localCertsDir) - e.ModuleCache.Set(CertsFileList, files) - return nil -} - -func generateCertsFiles(runtime connector.Runtime) []string { - var certsList []string - certsList = append(certsList, "ca.pem") - certsList = append(certsList, "ca-key.pem") - for _, host := range runtime.GetHostsByRole(common.ETCD) { - certsList = append(certsList, fmt.Sprintf("admin-%s.pem", host.GetName())) - certsList = append(certsList, fmt.Sprintf("admin-%s-key.pem", host.GetName())) - certsList = append(certsList, fmt.Sprintf("member-%s.pem", host.GetName())) - certsList = append(certsList, fmt.Sprintf("member-%s-key.pem", host.GetName())) - } - for _, host := range runtime.GetHostsByRole(common.Master) { - certsList = append(certsList, fmt.Sprintf("node-%s.pem", host.GetName())) - certsList = append(certsList, fmt.Sprintf("node-%s-key.pem", host.GetName())) - } - return certsList -} - type SyncCertsFile struct { common.KubeAction } diff --git a/pkg/etcd/templates/certs_script.go b/pkg/etcd/templates/certs_script.go deleted file mode 100644 index e1633a71..00000000 --- a/pkg/etcd/templates/certs_script.go +++ /dev/null @@ -1,142 +0,0 @@ -/* - Copyright 2021 The KubeSphere Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package templates - -import ( - "github.com/kubesphere/kubekey/pkg/core/connector" - "github.com/lithammer/dedent" - "strings" - "text/template" -) - -// EtcdSslTempl defines the template of the script for generating etcd certs. -var EtcdSslScript = template.Must(template.New("make-ssl-etcd.sh").Parse( - dedent.Dedent(`#!/bin/bash - -# Author: Smana smainklh@gmail.com -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -o errexit -set -o pipefail -usage() -{ - cat << EOF -Create self signed certificates - -Usage : $(basename $0) -f [-d ] - -h | --help : Show this message - -f | --config : Openssl configuration file - -d | --ssldir : Directory where the certificates will be installed - - ex : - $(basename $0) -f openssl.conf -d /srv/ssl -EOF -} - -# Options parsing -while (($#)); do - case "$1" in - -h | --help) usage; exit 0;; - -f | --config) CONFIG=${2}; shift 2;; - -d | --ssldir) SSLDIR="${2}"; shift 2;; - *) - usage - echo "ERROR : Unknown option" - exit 3 - ;; - esac -done - -if [ -z ${CONFIG} ]; then - echo "ERROR: the openssl configuration file is missing. option -f" - exit 1 -fi -if [ -z ${SSLDIR} ]; then - SSLDIR="/etc/ssl/etcd" -fi - -tmpdir=$(mktemp -d /tmp/etcd_cacert.XXXXXX) -trap 'rm -rf "${tmpdir}"' EXIT -cd "${tmpdir}" - -mkdir -p "${SSLDIR}" - -# Root CA -if [ -e "$SSLDIR/ca-key.pem" ]; then - # Reuse existing CA - cp $SSLDIR/{ca.pem,ca-key.pem} . -else - openssl genrsa -out ca-key.pem 2048 > /dev/null 2>&1 - openssl req -x509 -new -nodes -key ca-key.pem -days 36500 -out ca.pem -subj "/CN=etcd-ca" > /dev/null 2>&1 -fi - -MASTERS='{{ .Masters }}' -HOSTS='{{ .Hosts }}' - -# ETCD member -if [ -n "$MASTERS" ]; then - for host in $MASTERS; do - cn="${host%%.*}" - # Member key - openssl genrsa -out member-${host}-key.pem 2048 > /dev/null 2>&1 - openssl req -new -key member-${host}-key.pem -out member-${host}.csr -subj "/CN=etcd-member-${cn}" -config ${CONFIG} > /dev/null 2>&1 - openssl x509 -req -in member-${host}.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out member-${host}.pem -days 36500 -extensions ssl_client -extfile ${CONFIG} > /dev/null 2>&1 - - # Admin key - openssl genrsa -out admin-${host}-key.pem 2048 > /dev/null 2>&1 - openssl req -new -key admin-${host}-key.pem -out admin-${host}.csr -subj "/CN=etcd-admin-${cn}" > /dev/null 2>&1 - openssl x509 -req -in admin-${host}.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out admin-${host}.pem -days 36500 -extensions ssl_client -extfile ${CONFIG} > /dev/null 2>&1 - done -fi - -# Node keys -if [ -n "$HOSTS" ]; then - for host in $HOSTS; do - cn="${host%%.*}" - openssl genrsa -out node-${host}-key.pem 2048 > /dev/null 2>&1 - openssl req -new -key node-${host}-key.pem -out node-${host}.csr -subj "/CN=etcd-node-${cn}" > /dev/null 2>&1 - openssl x509 -req -in node-${host}.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out node-${host}.pem -days 36500 -extensions ssl_client -extfile ${CONFIG} > /dev/null 2>&1 - done -fi - -# Install certs -if [ -e "$SSLDIR/ca-key.pem" ]; then - # No pass existing CA - rm -f ca.pem ca-key.pem -fi - -mv *.pem ${SSLDIR}/ -`))) - -func GenerateHosts(hosts []connector.Host) string { - var res []string - for _, host := range hosts { - res = append(res, host.GetName()) - } - return strings.Join(res, " ") -} diff --git a/pkg/etcd/templates/openssl_config.go b/pkg/etcd/templates/openssl_config.go deleted file mode 100644 index 4dd879fb..00000000 --- a/pkg/etcd/templates/openssl_config.go +++ /dev/null @@ -1,86 +0,0 @@ -/* - Copyright 2021 The KubeSphere Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package templates - -import ( - kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/apis/kubekey/v1alpha2" - "github.com/kubesphere/kubekey/pkg/common" - "github.com/lithammer/dedent" - "text/template" -) - -// Add is used in the template to implement the addition operation. -func Add(a int, b int) int { - return a + b -} - -var ( - funcMap = template.FuncMap{"Add": Add} - - // EtcdSslCfgTempl defines the template of openssl's configuration for etcd. - ETCDOpenSSLConf = template.Must(template.New("openssl.conf").Funcs(funcMap).Parse( - dedent.Dedent(`[req] -req_extensions = v3_req -distinguished_name = req_distinguished_name - -[req_distinguished_name] - -[ v3_req ] -basicConstraints = CA:FALSE -keyUsage = nonRepudiation, digitalSignature, keyEncipherment -subjectAltName = @alt_names - -[ ssl_client ] -extendedKeyUsage = clientAuth, serverAuth -basicConstraints = CA:FALSE -subjectKeyIdentifier=hash -authorityKeyIdentifier=keyid,issuer -subjectAltName = @alt_names - -[ v3_ca ] -basicConstraints = CA:TRUE -keyUsage = nonRepudiation, digitalSignature, keyEncipherment -subjectAltName = @alt_names -authorityKeyIdentifier=keyid:always,issuer - -[alt_names] -{{- range $i, $v := .Dns }} -DNS.{{ Add $i 1 }} = {{ $v }} -{{- end }} -{{- range $i, $v := .Ips }} -IP.{{ Add $i 1 }} = {{ $v }} -{{- end }} - - `))) -) - -func DNSAndIp(kubeConf *common.KubeConf) (dns []string, ip []string) { - dnsList := []string{"localhost", "etcd.kube-system.svc.cluster.local", "etcd.kube-system.svc", "etcd.kube-system", "etcd"} - ipList := []string{"127.0.0.1"} - - if kubeConf.Cluster.ControlPlaneEndpoint.Domain == "" { - dnsList = append(dnsList, kubekeyapiv1alpha2.DefaultLBDomain) - } else { - dnsList = append(dnsList, kubeConf.Cluster.ControlPlaneEndpoint.Domain) - } - - for _, host := range kubeConf.Cluster.Hosts { - dnsList = append(dnsList, host.Name) - ipList = append(ipList, host.InternalAddress) - } - return dnsList, ipList -} diff --git a/pkg/utils/certs/certs.go b/pkg/utils/certs/certs.go new file mode 100644 index 00000000..78efbb7b --- /dev/null +++ b/pkg/utils/certs/certs.go @@ -0,0 +1,331 @@ +/* + Copyright 2021 The KubeSphere Authors. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package certs + +import ( + "crypto" + cryptorand "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "github.com/kubesphere/kubekey/pkg/common" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/sets" + certutil "k8s.io/client-go/util/cert" + "math" + "math/big" + "net" + "time" +) + +const ( + // CertificateValidity defines the validity for all the signed certificates generated by kubeadm + CertificateValidity = time.Hour * 24 * 365 * 10 + // CertificateBlockType is a possible value for pem.Block.Type. + CertificateBlockType = "CERTIFICATE" + rsaKeySize = 2048 +) + +// KubekeyCert represents a certificate that Kubeadm will create to function properly. +type KubekeyCert struct { + Name string + LongName string + BaseName string + CAName string + Config CertConfig +} + +// GetConfig returns the definition for the given cert given the provided InitConfiguration +func (k *KubekeyCert) GetConfig(_ *common.KubeConf) (*CertConfig, error) { + + return &k.Config, nil +} + +// CreateFromCA makes and writes a certificate using the given CA cert and key. +func (k *KubekeyCert) CreateFromCA(kubeConf *common.KubeConf, pkiPath string, caCert *x509.Certificate, caKey crypto.Signer) error { + cfg, err := k.GetConfig(kubeConf) + if err != nil { + return errors.Wrapf(err, "couldn't create %q certificate", k.Name) + } + cert, key, err := NewCertAndKey(caCert, caKey, cfg) + if err != nil { + return err + } + err = writeCertificateFilesIfNotExist( + pkiPath, + k.BaseName, + caCert, + cert, + key, + cfg, + ) + + if err != nil { + return errors.Wrapf(err, "failed to write or validate certificate %q", k.Name) + } + + return nil +} + +func GenerateCA(ca *KubekeyCert, pkiPath string, kubeConf *common.KubeConf) error { + + if cert, err := TryLoadCertFromDisk(pkiPath, ca.BaseName); err == nil { + CheckCertificatePeriodValidity(ca.BaseName, cert) + + if _, err := TryLoadKeyFromDisk(pkiPath, ca.BaseName); err == nil { + fmt.Printf("[certs] Using existing %s certificate authority\n", ca.BaseName) + return nil + } + fmt.Printf("[certs] Using existing %s keyless certificate authority\n", ca.BaseName) + return nil + } + + // create the new certificate authority (or use existing) + return CreateCACertAndKeyFiles(ca, pkiPath, kubeConf) + +} + +// CreateCACertAndKeyFiles generates and writes out a given certificate authority. +// The certSpec should be one of the variables from this package. +func CreateCACertAndKeyFiles(certSpec *KubekeyCert, pkiPath string, kubeConf *common.KubeConf) error { + if certSpec.CAName != "" { + return errors.Errorf("this function should only be used for CAs, but cert %s has CA %s", certSpec.Name, certSpec.CAName) + } + + certConfig, err := certSpec.GetConfig(kubeConf) + if err != nil { + return err + } + + caCert, caKey, err := NewCertificateAuthority(certConfig) + if err != nil { + return err + } + + return writeCertificateAuthorityFilesIfNotExist( + pkiPath, + certSpec.BaseName, + caCert, + caKey, + ) +} + +func GenerateCerts(cert *KubekeyCert, caCert *KubekeyCert, pkiPath string, kubeConf *common.KubeConf) error { + // TODO: if using external etcd, skips etcd certificates generation + + if certData, intermediates, err := TryLoadCertChainFromDisk(pkiPath, cert.BaseName); err == nil { + CheckCertificatePeriodValidity(cert.BaseName, certData) + + caCertData, err := TryLoadCertFromDisk(pkiPath, caCert.BaseName) + if err != nil { + return errors.Wrapf(err, "couldn't load CA certificate %s", caCert.Name) + } + + CheckCertificatePeriodValidity(caCert.BaseName, caCertData) + + if err := VerifyCertChain(certData, intermediates, caCertData); err != nil { + return errors.Wrapf(err, "[certs] certificate %s not signed by CA certificate %s", cert.BaseName, caCert.BaseName) + } + + fmt.Printf("[certs] Using existing %s certificate and key on disk\n", cert.BaseName) + return nil + } + + // create the new certificate (or use existing) + return CreateCertAndKeyFilesWithCA(caCert, cert, pkiPath, kubeConf) +} + +// CreateCertAndKeyFilesWithCA loads the given certificate authority from disk, then generates and writes out the given certificate and key. +// The certSpec and caCertSpec should both be one of the variables from this package. +func CreateCertAndKeyFilesWithCA(caCertSpec *KubekeyCert, certSpec *KubekeyCert, pkiPath string, kubeConf *common.KubeConf) error { + if certSpec.CAName != caCertSpec.Name { + return errors.Errorf("expected CAname for %s to be %q, but was %s", certSpec.Name, certSpec.CAName, caCertSpec.Name) + } + + caCert, caKey, err := LoadCertificateAuthority(pkiPath, caCertSpec.BaseName) + if err != nil { + return errors.Wrapf(err, "couldn't load CA certificate %s", caCertSpec.Name) + } + + return certSpec.CreateFromCA(kubeConf, pkiPath, caCert, caKey) +} + +// LoadCertificateAuthority tries to load a CA in the given directory with the given name. +func LoadCertificateAuthority(pkiDir string, baseName string) (*x509.Certificate, crypto.Signer, error) { + // Checks if certificate authority exists in the PKI directory + if !CertOrKeyExist(pkiDir, baseName) { + return nil, nil, errors.Errorf("couldn't load %s certificate authority from %s", baseName, pkiDir) + } + + // Try to load certificate authority .crt and .key from the PKI directory + caCert, caKey, err := TryLoadCertAndKeyFromDisk(pkiDir, baseName) + if err != nil { + return nil, nil, errors.Wrapf(err, "failure loading %s certificate authority", baseName) + } + // Validate period + CheckCertificatePeriodValidity(baseName, caCert) + + // Make sure the loaded CA cert actually is a CA + if !caCert.IsCA { + return nil, nil, errors.Errorf("%s certificate is not a certificate authority", baseName) + } + + return caCert, caKey, nil +} + +// NewCertAndKey creates new certificate and key by passing the certificate authority certificate and key +func NewCertAndKey(caCert *x509.Certificate, caKey crypto.Signer, config *CertConfig) (*x509.Certificate, crypto.Signer, error) { + if len(config.Usages) == 0 { + return nil, nil, errors.New("must specify at least one ExtKeyUsage") + } + + key, err := NewPrivateKey(config.PublicKeyAlgorithm) + if err != nil { + return nil, nil, errors.Wrap(err, "unable to create private key") + } + + cert, err := NewSignedCert(config, key, caCert, caKey, false) + if err != nil { + return nil, nil, errors.Wrap(err, "unable to sign certificate") + } + + return cert, key, nil +} + +// NewSignedCert creates a signed certificate using the given CA certificate and key +func NewSignedCert(cfg *CertConfig, key crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer, isCA bool) (*x509.Certificate, error) { + serial, err := cryptorand.Int(cryptorand.Reader, new(big.Int).SetInt64(math.MaxInt64)) + if err != nil { + return nil, err + } + if len(cfg.CommonName) == 0 { + return nil, errors.New("must specify a CommonName") + } + + keyUsage := x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature + if isCA { + keyUsage |= x509.KeyUsageCertSign + } + + RemoveDuplicateAltNames(&cfg.AltNames) + + notAfter := time.Now().Add(CertificateValidity).UTC() + if cfg.NotAfter != nil { + notAfter = *cfg.NotAfter + } + + certTmpl := x509.Certificate{ + Subject: pkix.Name{ + CommonName: cfg.CommonName, + Organization: cfg.Organization, + }, + DNSNames: cfg.AltNames.DNSNames, + IPAddresses: cfg.AltNames.IPs, + SerialNumber: serial, + NotBefore: caCert.NotBefore, + NotAfter: notAfter, + KeyUsage: keyUsage, + ExtKeyUsage: cfg.Usages, + BasicConstraintsValid: true, + IsCA: isCA, + } + certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &certTmpl, caCert, key.Public(), caKey) + if err != nil { + return nil, err + } + return x509.ParseCertificate(certDERBytes) +} + +// RemoveDuplicateAltNames removes duplicate items in altNames. +func RemoveDuplicateAltNames(altNames *certutil.AltNames) { + if altNames == nil { + return + } + + if altNames.DNSNames != nil { + altNames.DNSNames = sets.NewString(altNames.DNSNames...).List() + } + + ipsKeys := make(map[string]struct{}) + var ips []net.IP + for _, one := range altNames.IPs { + if _, ok := ipsKeys[one.String()]; !ok { + ipsKeys[one.String()] = struct{}{} + ips = append(ips, one) + } + } + altNames.IPs = ips +} + +// CheckCertificatePeriodValidity takes a certificate and prints a warning if its period +// is not valid related to the current time. It does so only if the certificate was not validated already +// by keeping track with a cache. +func CheckCertificatePeriodValidity(baseName string, cert *x509.Certificate) { + certPeriodValidationMutex.Lock() + defer certPeriodValidationMutex.Unlock() + if _, exists := certPeriodValidation[baseName]; exists { + return + } + certPeriodValidation[baseName] = struct{}{} + + if err := ValidateCertPeriod(cert, 0); err != nil { + logrus.Warningf("WARNING: could not validate bounds for certificate %s: %v", baseName, err) + } +} + +// writeCertificateFilesIfNotExist write a new certificate to the given path. +// If there already is a certificate file at the given path; kubeadm tries to load it and check if the values in the +// existing and the expected certificate equals. If they do; kubeadm will just skip writing the file as it's up-to-date, +// otherwise this function returns an error. +func writeCertificateFilesIfNotExist(pkiDir string, baseName string, signingCert *x509.Certificate, cert *x509.Certificate, key crypto.Signer, cfg *CertConfig) error { + + // Checks if the signed certificate exists in the PKI directory + if CertOrKeyExist(pkiDir, baseName) { + // Try to load key from the PKI directory + _, err := TryLoadKeyFromDisk(pkiDir, baseName) + if err != nil { + return errors.Wrapf(err, "failure loading %s key", baseName) + } + + // Try to load certificate from the PKI directory + signedCert, intermediates, err := TryLoadCertChainFromDisk(pkiDir, baseName) + if err != nil { + return errors.Wrapf(err, "failure loading %s certificate", baseName) + } + // Validate period + CheckCertificatePeriodValidity(baseName, signedCert) + + // Check if the existing cert is signed by the given CA + if err := VerifyCertChain(signedCert, intermediates, signingCert); err != nil { + return errors.Errorf("certificate %s is not signed by corresponding CA", baseName) + } + + // Check if the certificate has the correct attributes + if err := validateCertificateWithConfig(signedCert, baseName, cfg); err != nil { + return err + } + + fmt.Printf("[certs] Using the existing %q certificate and key\n", baseName) + } else { + if err := WriteCertAndKey(pkiDir, baseName, cert, key); err != nil { + return errors.Wrapf(err, "failure while saving %s certificate and key", baseName) + } + if HasServerAuth(cert) { + fmt.Printf("[certs] %s serving cert is signed for DNS names %v and IPs %v\n", baseName, cert.DNSNames, cert.IPAddresses) + } + } + + return nil +} diff --git a/pkg/utils/certs/utils.go b/pkg/utils/certs/utils.go new file mode 100644 index 00000000..0edab8c7 --- /dev/null +++ b/pkg/utils/certs/utils.go @@ -0,0 +1,320 @@ +/* +Copyright 2016 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package certs + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + cryptorand "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "github.com/pkg/errors" + certutil "k8s.io/client-go/util/cert" + "k8s.io/client-go/util/keyutil" + "os" + "path/filepath" + "sync" + "time" +) + +// CertOrKeyExist returns a boolean whether the cert or the key exists +func CertOrKeyExist(pkiPath, name string) bool { + certificatePath, privateKeyPath := PathsForCertAndKey(pkiPath, name) + + _, certErr := os.Stat(certificatePath) + _, keyErr := os.Stat(privateKeyPath) + if os.IsNotExist(certErr) && os.IsNotExist(keyErr) { + // The cert and the key do not exist + return false + } + + // Both files exist or one of them + return true +} + +// PathsForCertAndKey returns the paths for the certificate and key given the path and basename. +func PathsForCertAndKey(pkiPath, name string) (string, string) { + return pathForCert(pkiPath, name), pathForKey(pkiPath, name) +} + +func pathForCert(pkiPath, name string) string { + return filepath.Join(pkiPath, fmt.Sprintf("%s.pem", name)) +} + +func pathForKey(pkiPath, name string) string { + return filepath.Join(pkiPath, fmt.Sprintf("%s-key.pem", name)) +} + +// TryLoadKeyFromDisk tries to load the key from the disk and validates that it is valid +func TryLoadKeyFromDisk(pkiPath, name string) (crypto.Signer, error) { + privateKeyPath := pathForKey(pkiPath, name) + + // Parse the private key from a file + privKey, err := keyutil.PrivateKeyFromFile(privateKeyPath) + if err != nil { + return nil, errors.Wrapf(err, "couldn't load the private key file %s", privateKeyPath) + } + + // Allow RSA and ECDSA formats only + var key crypto.Signer + switch k := privKey.(type) { + case *rsa.PrivateKey: + key = k + case *ecdsa.PrivateKey: + key = k + default: + return nil, errors.Errorf("the private key file %s is neither in RSA nor ECDSA format", privateKeyPath) + } + + return key, nil +} + +// TryLoadCertChainFromDisk tries to load the cert chain from the disk +func TryLoadCertChainFromDisk(pkiPath, name string) (*x509.Certificate, []*x509.Certificate, error) { + certificatePath := pathForCert(pkiPath, name) + + certs, err := certutil.CertsFromFile(certificatePath) + if err != nil { + return nil, nil, errors.Wrapf(err, "couldn't load the certificate file %s", certificatePath) + } + + cert := certs[0] + intermediates := certs[1:] + + return cert, intermediates, nil +} + +// VerifyCertChain verifies that a certificate has a valid chain of +// intermediate CAs back to the root CA +func VerifyCertChain(cert *x509.Certificate, intermediates []*x509.Certificate, root *x509.Certificate) error { + rootPool := x509.NewCertPool() + rootPool.AddCert(root) + + intermediatePool := x509.NewCertPool() + for _, c := range intermediates { + intermediatePool.AddCert(c) + } + + verifyOptions := x509.VerifyOptions{ + Roots: rootPool, + Intermediates: intermediatePool, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + } + + if _, err := cert.Verify(verifyOptions); err != nil { + return err + } + + return nil +} + +// TryLoadCertAndKeyFromDisk tries to load a cert and a key from the disk and validates that they are valid +func TryLoadCertAndKeyFromDisk(pkiPath, name string) (*x509.Certificate, crypto.Signer, error) { + cert, err := TryLoadCertFromDisk(pkiPath, name) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to load certificate") + } + + key, err := TryLoadKeyFromDisk(pkiPath, name) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to load key") + } + + return cert, key, nil +} + +// TryLoadCertFromDisk tries to load the cert from the disk +func TryLoadCertFromDisk(pkiPath, name string) (*x509.Certificate, error) { + certificatePath := pathForCert(pkiPath, name) + + certs, err := certutil.CertsFromFile(certificatePath) + if err != nil { + return nil, errors.Wrapf(err, "couldn't load the certificate file %s", certificatePath) + } + + // We are only putting one certificate in the certificate pem file, so it's safe to just pick the first one + // TODO: Support multiple certs here in order to be able to rotate certs + cert := certs[0] + + return cert, nil +} + +// CertConfig is a wrapper around certutil.Config extending it with PublicKeyAlgorithm. +type CertConfig struct { + certutil.Config + NotAfter *time.Time + PublicKeyAlgorithm x509.PublicKeyAlgorithm +} + +var ( + // certPeriodValidation is used to store if period validation was done for a certificate + certPeriodValidationMutex sync.Mutex + certPeriodValidation = map[string]struct{}{} +) + +// NewPrivateKey returns a new private key. +var NewPrivateKey = GeneratePrivateKey + +func GeneratePrivateKey(keyType x509.PublicKeyAlgorithm) (crypto.Signer, error) { + if keyType == x509.ECDSA { + return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader) + } + + return rsa.GenerateKey(cryptorand.Reader, rsaKeySize) +} + +// NewCertificateAuthority creates new certificate and private key for the certificate authority +func NewCertificateAuthority(config *CertConfig) (*x509.Certificate, crypto.Signer, error) { + key, err := NewPrivateKey(config.PublicKeyAlgorithm) + if err != nil { + return nil, nil, errors.Wrap(err, "unable to create private key while generating CA certificate") + } + + cert, err := certutil.NewSelfSignedCACert(config.Config, key) + if err != nil { + return nil, nil, errors.Wrap(err, "unable to create self-signed CA certificate") + } + + return cert, key, nil +} + +// writeCertificateAuthorityFilesIfNotExist write a new certificate Authority to the given path. +// If there already is a certificate file at the given path; kubeadm tries to load it and check if the values in the +// existing and the expected certificate equals. If they do; kubeadm will just skip writing the file as it's up-to-date, +// otherwise this function returns an error. +func writeCertificateAuthorityFilesIfNotExist(pkiDir string, baseName string, caCert *x509.Certificate, caKey crypto.Signer) error { + + // If cert or key exists, we should try to load them + if CertOrKeyExist(pkiDir, baseName) { + + // Try to load .crt and .key from the PKI directory + caCert, _, err := TryLoadCertAndKeyFromDisk(pkiDir, baseName) + if err != nil { + return errors.Wrapf(err, "failure loading %s certificate", baseName) + } + // Validate period + CheckCertificatePeriodValidity(baseName, caCert) + + // Check if the existing cert is a CA + if !caCert.IsCA { + return errors.Errorf("certificate %s is not a CA", baseName) + } + + // kubeadm doesn't validate the existing certificate Authority more than this; + // Basically, if we find a certificate file with the same path; and it is a CA + // kubeadm thinks those files are equal and doesn't bother writing a new file + fmt.Printf("[certs] Using the existing %q certificate and key\n", baseName) + } else { + // Write .crt and .key files to disk + fmt.Printf("[certs] Generating %q certificate and key\n", baseName) + + if err := WriteCertAndKey(pkiDir, baseName, caCert, caKey); err != nil { + return errors.Wrapf(err, "failure while saving %s certificate and key", baseName) + } + } + return nil +} + +// validateCertificateWithConfig makes sure that a given certificate is valid at +// least for the SANs defined in the configuration. +func validateCertificateWithConfig(cert *x509.Certificate, baseName string, cfg *CertConfig) error { + for _, dnsName := range cfg.AltNames.DNSNames { + if err := cert.VerifyHostname(dnsName); err != nil { + return errors.Wrapf(err, "certificate %s is invalid", baseName) + } + } + for _, ipAddress := range cfg.AltNames.IPs { + if err := cert.VerifyHostname(ipAddress.String()); err != nil { + return errors.Wrapf(err, "certificate %s is invalid", baseName) + } + } + return nil +} + +// WriteCertAndKey stores certificate and key at the specified location +func WriteCertAndKey(pkiPath string, name string, cert *x509.Certificate, key crypto.Signer) error { + if err := WriteKey(pkiPath, name, key); err != nil { + return errors.Wrap(err, "couldn't write key") + } + + return WriteCert(pkiPath, name, cert) +} + +// HasServerAuth returns true if the given certificate is a ServerAuth +func HasServerAuth(cert *x509.Certificate) bool { + for i := range cert.ExtKeyUsage { + if cert.ExtKeyUsage[i] == x509.ExtKeyUsageServerAuth { + return true + } + } + return false +} + +// ValidateCertPeriod checks if the certificate is valid relative to the current time +// (+/- offset) +func ValidateCertPeriod(cert *x509.Certificate, offset time.Duration) error { + period := fmt.Sprintf("NotBefore: %v, NotAfter: %v", cert.NotBefore, cert.NotAfter) + now := time.Now().Add(offset) + if now.Before(cert.NotBefore) { + return errors.Errorf("the certificate is not valid yet: %s", period) + } + if now.After(cert.NotAfter) { + return errors.Errorf("the certificate has expired: %s", period) + } + return nil +} + +// WriteKey stores the given key at the given location +func WriteKey(pkiPath, name string, key crypto.Signer) error { + if key == nil { + return errors.New("private key cannot be nil when writing to file") + } + + privateKeyPath := pathForKey(pkiPath, name) + encoded, err := keyutil.MarshalPrivateKeyToPEM(key) + if err != nil { + return errors.Wrapf(err, "unable to marshal private key to PEM") + } + if err := keyutil.WriteKey(privateKeyPath, encoded); err != nil { + return errors.Wrapf(err, "unable to write private key to file %s", privateKeyPath) + } + + return nil +} + +// WriteCert stores the given certificate at the given location +func WriteCert(pkiPath, name string, cert *x509.Certificate) error { + if cert == nil { + return errors.New("certificate cannot be nil when writing to file") + } + + certificatePath := pathForCert(pkiPath, name) + if err := certutil.WriteCert(certificatePath, EncodeCertPEM(cert)); err != nil { + return errors.Wrapf(err, "unable to write certificate to file %s", certificatePath) + } + + return nil +} + +// EncodeCertPEM returns PEM-endcoded certificate data +func EncodeCertPEM(cert *x509.Certificate) []byte { + block := pem.Block{ + Type: CertificateBlockType, + Bytes: cert.Raw, + } + return pem.EncodeToMemory(&block) +}