Support kubekey to independently generate certificates

Signed-off-by: pixiake <guofeng@yunify.com>
This commit is contained in:
pixiake 2021-12-29 11:59:05 +08:00
parent dcdd770619
commit ba620e7320
11 changed files with 891 additions and 333 deletions

View File

@ -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")

2
go.mod
View File

@ -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

View File

@ -54,6 +54,7 @@ type Argument struct {
KubeConfig string
Artifact string
SkipInstallPackages bool
CertificatesDir string
}
func NewKubeRuntime(flag string, arg Argument) (*KubeRuntime, error) {

View File

@ -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 {

208
pkg/etcd/certs.go Normal file
View File

@ -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
}

View File

@ -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,
}

View File

@ -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
}

View File

@ -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 <config> [-d <ssldir>]
-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, " ")
}

View File

@ -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
}

331
pkg/utils/certs/certs.go Normal file
View File

@ -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
}

320
pkg/utils/certs/utils.go Normal file
View File

@ -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)
}