mirror of
https://github.com/kubesphere/kubekey.git
synced 2025-12-26 01:22:51 +00:00
Support kubekey to independently generate certificates
Signed-off-by: pixiake <guofeng@yunify.com>
This commit is contained in:
parent
dcdd770619
commit
ba620e7320
|
|
@ -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
2
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
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ type Argument struct {
|
|||
KubeConfig string
|
||||
Artifact string
|
||||
SkipInstallPackages bool
|
||||
CertificatesDir string
|
||||
}
|
||||
|
||||
func NewKubeRuntime(flag string, arg Argument) (*KubeRuntime, error) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, " ")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in New Issue