mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 15:05:27 +00:00
Merge pull request #30922 from yifan-gu/tls_bootstrap_refactor
Automatic merge from submit-queue Implement TLS bootstrap for kubelet using `--experimental-bootstrap-kubeconfig` (2nd take) Ref kubernetes/features#43 (comment) cc @gtank @philips @mikedanese @aaronlevy @liggitt @deads2k @errordeveloper @justinsb Continue on the older PR https://github.com/kubernetes/kubernetes/pull/30094 as there are too many comments on that one and it's not loadable now.
This commit is contained in:
commit
a41e6e3817
259
cmd/kubelet/app/bootstrap.go
Normal file
259
cmd/kubelet/app/bootstrap.go
Normal file
@ -0,0 +1,259 @@
|
||||
/*
|
||||
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 app
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||
"k8s.io/kubernetes/pkg/apis/certificates"
|
||||
unversionedcertificates "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/certificates/unversioned"
|
||||
"k8s.io/kubernetes/pkg/client/restclient"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
|
||||
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
|
||||
"k8s.io/kubernetes/pkg/fields"
|
||||
utilcertificates "k8s.io/kubernetes/pkg/util/certificates"
|
||||
"k8s.io/kubernetes/pkg/util/crypto"
|
||||
"k8s.io/kubernetes/pkg/watch"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultKubeletClientCertificateFile = "kubelet-client.crt"
|
||||
defaultKubeletClientKeyFile = "kubelet-client.key"
|
||||
)
|
||||
|
||||
// bootstrapClientCert requests a client cert for kubelet if the kubeconfigPath file does not exist.
|
||||
// The kubeconfig at bootstrapPath is used to request a client certificate from the API server.
|
||||
// On success, a kubeconfig file referencing the generated key and obtained certificate is written to kubeconfigPath.
|
||||
// The certificate and key file are stored in certDir.
|
||||
func bootstrapClientCert(kubeconfigPath string, bootstrapPath string, certDir string, nodeName string) error {
|
||||
// Short-circuit if the kubeconfig file already exists.
|
||||
// TODO: inspect the kubeconfig, ensure a rest client can be built from it, verify client cert expiration, etc.
|
||||
_, err := os.Stat(kubeconfigPath)
|
||||
if err == nil {
|
||||
glog.V(2).Infof("Kubeconfig %s exists, skipping bootstrap", kubeconfigPath)
|
||||
return nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
glog.Errorf("Error reading kubeconfig %s, skipping bootstrap: %v", kubeconfigPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
glog.V(2).Info("Using bootstrap kubeconfig to generate TLS client cert, key and kubeconfig file")
|
||||
|
||||
bootstrapClientConfig, err := loadRESTClientConfig(bootstrapPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load bootstrap kubeconfig: %v", err)
|
||||
}
|
||||
bootstrapClient, err := unversionedcertificates.NewForConfig(bootstrapClientConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create certificates signing request client: %v", err)
|
||||
}
|
||||
|
||||
success := false
|
||||
|
||||
// Get the private key.
|
||||
keyPath, err := filepath.Abs(filepath.Join(certDir, defaultKubeletClientKeyFile))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to build bootstrap key path: %v", err)
|
||||
}
|
||||
keyData, generatedKeyFile, err := loadOrGenerateKeyFile(keyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if generatedKeyFile {
|
||||
defer func() {
|
||||
if !success {
|
||||
if err := os.Remove(keyPath); err != nil {
|
||||
glog.Warningf("Cannot clean up the key file %q: %v", keyPath, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Get the cert.
|
||||
certPath, err := filepath.Abs(filepath.Join(certDir, defaultKubeletClientCertificateFile))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to build bootstrap client cert path: %v", err)
|
||||
}
|
||||
certData, err := RequestClientCertificate(bootstrapClient.CertificateSigningRequests(), keyData, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := crypto.WriteCertToPath(certPath, certData); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if !success {
|
||||
if err := os.Remove(certPath); err != nil {
|
||||
glog.Warningf("Cannot clean up the cert file %q: %v", certPath, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Get the CA data from the bootstrap client config.
|
||||
caFile, caData := bootstrapClientConfig.CAFile, []byte{}
|
||||
if len(caFile) == 0 {
|
||||
caData = bootstrapClientConfig.CAData
|
||||
}
|
||||
|
||||
// Build resulting kubeconfig.
|
||||
kubeconfigData := clientcmdapi.Config{
|
||||
// Define a cluster stanza based on the bootstrap kubeconfig.
|
||||
Clusters: map[string]*clientcmdapi.Cluster{"default-cluster": {
|
||||
Server: bootstrapClientConfig.Host,
|
||||
InsecureSkipTLSVerify: bootstrapClientConfig.Insecure,
|
||||
CertificateAuthority: caFile,
|
||||
CertificateAuthorityData: caData,
|
||||
}},
|
||||
// Define auth based on the obtained client cert.
|
||||
AuthInfos: map[string]*clientcmdapi.AuthInfo{"default-auth": {
|
||||
ClientCertificate: certPath,
|
||||
ClientKey: keyPath,
|
||||
}},
|
||||
// Define a context that connects the auth info and cluster, and set it as the default
|
||||
Contexts: map[string]*clientcmdapi.Context{"default-context": {
|
||||
Cluster: "default-cluster",
|
||||
AuthInfo: "default-auth",
|
||||
Namespace: "default",
|
||||
}},
|
||||
CurrentContext: "default-context",
|
||||
}
|
||||
|
||||
// Marshal to disk
|
||||
if err := clientcmd.WriteToFile(kubeconfigData, kubeconfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
success = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadRESTClientConfig(kubeconfig string) (*restclient.Config, error) {
|
||||
// Load structured kubeconfig data from the given path.
|
||||
loader := &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}
|
||||
loadedConfig, err := loader.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Flatten the loaded data to a particular restclient.Config based on the current context.
|
||||
return clientcmd.NewNonInteractiveClientConfig(
|
||||
*loadedConfig,
|
||||
loadedConfig.CurrentContext,
|
||||
&clientcmd.ConfigOverrides{},
|
||||
loader,
|
||||
).ClientConfig()
|
||||
}
|
||||
|
||||
func loadOrGenerateKeyFile(keyPath string) (data []byte, wasGenerated bool, err error) {
|
||||
loadedData, err := ioutil.ReadFile(keyPath)
|
||||
if err == nil {
|
||||
return loadedData, false, err
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, false, fmt.Errorf("error loading key from %s: %v", keyPath, err)
|
||||
}
|
||||
|
||||
generatedData, err := utilcertificates.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error generating key: %v", err)
|
||||
}
|
||||
if err := crypto.WriteKeyToPath(keyPath, generatedData); err != nil {
|
||||
return nil, false, fmt.Errorf("error writing key to %s: %v", keyPath, err)
|
||||
}
|
||||
return generatedData, true, nil
|
||||
}
|
||||
|
||||
// RequestClientCertificate will create a certificate signing request and send it to API server,
|
||||
// then it will watch the object's status, once approved by API server, it will return the API
|
||||
// server's issued certificate (pem-encoded). If there is any errors, or the watch timeouts,
|
||||
// it will return an error.
|
||||
func RequestClientCertificate(client unversionedcertificates.CertificateSigningRequestInterface, privateKeyData []byte, nodeName string) (certData []byte, err error) {
|
||||
subject := &pkix.Name{
|
||||
Organization: []string{"system:nodes"},
|
||||
CommonName: fmt.Sprintf("system:node:%s", nodeName),
|
||||
}
|
||||
|
||||
privateKey, err := utilcertificates.ParsePrivateKey(privateKeyData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid private key for certificate request: %v", err)
|
||||
}
|
||||
csr, err := utilcertificates.NewCertificateRequest(privateKey, subject, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to generate certificate request: %v", err)
|
||||
}
|
||||
|
||||
req, err := client.Create(&certificates.CertificateSigningRequest{
|
||||
// Username, UID, Groups will be injected by API server.
|
||||
TypeMeta: unversioned.TypeMeta{Kind: "CertificateSigningRequest"},
|
||||
ObjectMeta: api.ObjectMeta{GenerateName: "csr-"},
|
||||
|
||||
// TODO: For now, this is a request for a certificate with allowed usage of "TLS Web Client Authentication".
|
||||
// Need to figure out whether/how to surface the allowed usage in the spec.
|
||||
Spec: certificates.CertificateSigningRequestSpec{Request: csr},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create certificate signing request: %v", err)
|
||||
|
||||
}
|
||||
|
||||
// Make a default timeout = 3600s.
|
||||
var defaultTimeoutSeconds int64 = 3600
|
||||
resultCh, err := client.Watch(api.ListOptions{
|
||||
Watch: true,
|
||||
TimeoutSeconds: &defaultTimeoutSeconds,
|
||||
FieldSelector: fields.OneTermEqualSelector("metadata.name", req.Name),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot watch on the certificate signing request: %v", err)
|
||||
}
|
||||
|
||||
var status certificates.CertificateSigningRequestStatus
|
||||
ch := resultCh.ResultChan()
|
||||
|
||||
for {
|
||||
event, ok := <-ch
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
if event.Type == watch.Modified || event.Type == watch.Added {
|
||||
if event.Object.(*certificates.CertificateSigningRequest).UID != req.UID {
|
||||
continue
|
||||
}
|
||||
status = event.Object.(*certificates.CertificateSigningRequest).Status
|
||||
for _, c := range status.Conditions {
|
||||
if c.Type == certificates.CertificateDenied {
|
||||
return nil, fmt.Errorf("certificate signing request is not approved, reason: %v, message: %v", c.Reason, c.Message)
|
||||
}
|
||||
if c.Type == certificates.CertificateApproved && status.Certificate != nil {
|
||||
return status.Certificate, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("watch channel closed")
|
||||
}
|
85
cmd/kubelet/app/bootstrap_test.go
Normal file
85
cmd/kubelet/app/bootstrap_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
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 app
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/client/restclient"
|
||||
"k8s.io/kubernetes/pkg/util/diff"
|
||||
)
|
||||
|
||||
func TestLoadRESTClientConfig(t *testing.T) {
|
||||
testData := []byte(`
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority: ca-a.crt
|
||||
server: https://cluster-a.com
|
||||
name: cluster-a
|
||||
- cluster:
|
||||
certificate-authority-data: VGVzdA==
|
||||
server: https://cluster-b.com
|
||||
name: cluster-b
|
||||
contexts:
|
||||
- context:
|
||||
cluster: cluster-a
|
||||
namespace: ns-a
|
||||
user: user-a
|
||||
name: context-a
|
||||
- context:
|
||||
cluster: cluster-b
|
||||
namespace: ns-b
|
||||
user: user-b
|
||||
name: context-b
|
||||
current-context: context-b
|
||||
users:
|
||||
- name: user-a
|
||||
user:
|
||||
token: mytoken-a
|
||||
- name: user-b
|
||||
user:
|
||||
token: mytoken-b
|
||||
`)
|
||||
f, err := ioutil.TempFile("", "kubeconfig")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
ioutil.WriteFile(f.Name(), testData, os.FileMode(0755))
|
||||
|
||||
config, err := loadRESTClientConfig(f.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedConfig := &restclient.Config{
|
||||
Host: "https://cluster-b.com",
|
||||
TLSClientConfig: restclient.TLSClientConfig{
|
||||
CAData: []byte(`Test`),
|
||||
},
|
||||
BearerToken: "mytoken-b",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(config, expectedConfig) {
|
||||
t.Errorf("Unexpected config: %s", diff.ObjectDiff(config, expectedConfig))
|
||||
}
|
||||
}
|
@ -41,7 +41,9 @@ const (
|
||||
type KubeletServer struct {
|
||||
componentconfig.KubeletConfiguration
|
||||
|
||||
KubeConfig util.StringFlag
|
||||
KubeConfig util.StringFlag
|
||||
BootstrapKubeconfig string
|
||||
|
||||
// If true, an invalid KubeConfig will result in the Kubelet exiting with an error.
|
||||
RequireKubeConfig bool
|
||||
AuthPath util.StringFlag // Deprecated -- use KubeConfig instead
|
||||
@ -97,6 +99,10 @@ func (s *KubeletServer) AddFlags(fs *pflag.FlagSet) {
|
||||
fs.StringVar(&s.TLSPrivateKeyFile, "tls-private-key-file", s.TLSPrivateKeyFile, "File containing x509 private key matching --tls-cert-file.")
|
||||
fs.StringVar(&s.CertDirectory, "cert-dir", s.CertDirectory, "The directory where the TLS certs are located (by default /var/run/kubernetes). "+
|
||||
"If --tls-cert-file and --tls-private-key-file are provided, this flag will be ignored.")
|
||||
fs.StringVar(&s.BootstrapKubeconfig, "experimental-bootstrap-kubeconfig", s.BootstrapKubeconfig, "<Warning: Experimental feature> Path to a kubeconfig file that will be used to get client certificate for kubelet. "+
|
||||
"If the file specified by --kubeconfig does not exist, the bootstrap kubeconfig is used to request a client certificate from the API server. "+
|
||||
"On success, a kubeconfig file referencing the generated key and obtained certificate is written to the path specified by --kubeconfig. "+
|
||||
"The certificate and key file will be stored in the directory pointed by --cert-dir.")
|
||||
fs.StringVar(&s.HostnameOverride, "hostname-override", s.HostnameOverride, "If non-empty, will use this string as identification instead of the actual hostname.")
|
||||
fs.StringVar(&s.PodInfraContainerImage, "pod-infra-container-image", s.PodInfraContainerImage, "The image whose network/ipc namespaces containers in each pod will use.")
|
||||
fs.StringVar(&s.DockerEndpoint, "docker-endpoint", s.DockerEndpoint, "Use this for the docker endpoint to communicate with")
|
||||
|
@ -350,6 +350,33 @@ func run(s *options.KubeletServer, kcfg *KubeletConfig) (err error) {
|
||||
|
||||
if kcfg == nil {
|
||||
var kubeClient, eventClient *clientset.Clientset
|
||||
var autoDetectCloudProvider bool
|
||||
var cloud cloudprovider.Interface
|
||||
|
||||
if s.CloudProvider == kubeExternal.AutoDetectCloudProvider {
|
||||
autoDetectCloudProvider = true
|
||||
} else {
|
||||
cloud, err = cloudprovider.InitCloudProvider(s.CloudProvider, s.CloudConfigFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cloud == nil {
|
||||
glog.V(2).Infof("No cloud provider specified: %q from the config file: %q\n", s.CloudProvider, s.CloudConfigFile)
|
||||
} else {
|
||||
glog.V(2).Infof("Successfully initialized cloud provider: %q from the config file: %q\n", s.CloudProvider, s.CloudConfigFile)
|
||||
}
|
||||
}
|
||||
|
||||
if s.BootstrapKubeconfig != "" {
|
||||
nodeName, err := getNodeName(cloud, nodeutil.GetHostname(s.HostnameOverride))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bootstrapClientCert(s.KubeConfig.Value(), s.BootstrapKubeconfig, s.CertDirectory, nodeName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
clientConfig, err := CreateAPIServerClientConfig(s)
|
||||
if err == nil {
|
||||
kubeClient, err = clientset.NewForConfig(clientConfig)
|
||||
@ -374,24 +401,12 @@ func run(s *options.KubeletServer, kcfg *KubeletConfig) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kcfg = cfg
|
||||
kcfg.AutoDetectCloudProvider = autoDetectCloudProvider
|
||||
kcfg.Cloud = cloud
|
||||
kcfg.KubeClient = kubeClient
|
||||
kcfg.EventClient = eventClient
|
||||
|
||||
if s.CloudProvider == kubeExternal.AutoDetectCloudProvider {
|
||||
kcfg.AutoDetectCloudProvider = true
|
||||
} else {
|
||||
cloud, err := cloudprovider.InitCloudProvider(s.CloudProvider, s.CloudConfigFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cloud == nil {
|
||||
glog.V(2).Infof("No cloud provider specified: %q from the config file: %q\n", s.CloudProvider, s.CloudConfigFile)
|
||||
} else {
|
||||
glog.V(2).Infof("Successfully initialized cloud provider: %q from the config file: %q\n", s.CloudProvider, s.CloudConfigFile)
|
||||
kcfg.Cloud = cloud
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if kcfg.CAdvisorInterface == nil {
|
||||
@ -454,13 +469,35 @@ func run(s *options.KubeletServer, kcfg *KubeletConfig) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getNodeName returns the node name according to the cloud provider
|
||||
// if cloud provider is specified. Otherwise, returns the hostname of the node.
|
||||
func getNodeName(cloud cloudprovider.Interface, hostname string) (string, error) {
|
||||
if cloud == nil {
|
||||
return hostname, nil
|
||||
}
|
||||
|
||||
instances, ok := cloud.Instances()
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to get instances from cloud provider")
|
||||
}
|
||||
|
||||
nodeName, err := instances.CurrentNodeName(hostname)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching current instance name from cloud provider: %v", err)
|
||||
}
|
||||
|
||||
glog.V(2).Infof("cloud provider determined current node name to be %s", nodeName)
|
||||
|
||||
return nodeName, nil
|
||||
}
|
||||
|
||||
// InitializeTLS checks for a configured TLSCertFile and TLSPrivateKeyFile: if unspecified a new self-signed
|
||||
// certificate and key file are generated. Returns a configured server.TLSOptions object.
|
||||
func InitializeTLS(s *options.KubeletServer) (*server.TLSOptions, error) {
|
||||
if s.TLSCertFile == "" && s.TLSPrivateKeyFile == "" {
|
||||
s.TLSCertFile = path.Join(s.CertDirectory, "kubelet.crt")
|
||||
s.TLSPrivateKeyFile = path.Join(s.CertDirectory, "kubelet.key")
|
||||
if crypto.ShouldGenSelfSignedCerts(s.TLSCertFile, s.TLSPrivateKeyFile) {
|
||||
if !crypto.FoundCertOrKey(s.TLSCertFile, s.TLSPrivateKeyFile) {
|
||||
if err := crypto.GenerateSelfSignedCert(nodeutil.GetHostname(s.HostnameOverride), s.TLSCertFile, s.TLSPrivateKeyFile, nil, nil); err != nil {
|
||||
return nil, fmt.Errorf("unable to generate self signed cert: %v", err)
|
||||
}
|
||||
@ -693,23 +730,10 @@ func RunKubelet(kcfg *KubeletConfig) error {
|
||||
kcfg.Hostname = nodeutil.GetHostname(kcfg.HostnameOverride)
|
||||
|
||||
if len(kcfg.NodeName) == 0 {
|
||||
// Query the cloud provider for our node name, default to Hostname
|
||||
nodeName := kcfg.Hostname
|
||||
if kcfg.Cloud != nil {
|
||||
var err error
|
||||
instances, ok := kcfg.Cloud.Instances()
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to get instances from cloud provider")
|
||||
}
|
||||
|
||||
nodeName, err = instances.CurrentNodeName(kcfg.Hostname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching current instance name from cloud provider: %v", err)
|
||||
}
|
||||
|
||||
glog.V(2).Infof("cloud provider determined current node name to be %s", nodeName)
|
||||
nodeName, err := getNodeName(kcfg.Cloud, kcfg.Hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kcfg.NodeName = nodeName
|
||||
}
|
||||
|
||||
|
@ -163,6 +163,7 @@ executor-logv
|
||||
executor-path
|
||||
executor-suicide-timeout
|
||||
exit-on-lock-contention
|
||||
experimental-bootstrap-kubeconfig
|
||||
experimental-flannel-overlay
|
||||
experimental-keystone-url
|
||||
experimental-nvidia-gpus
|
||||
|
@ -723,7 +723,7 @@ func (s *GenericAPIServer) Run(options *options.ServerRunOptions) {
|
||||
alternateDNS := []string{"kubernetes.default.svc", "kubernetes.default", "kubernetes"}
|
||||
// It would be nice to set a fqdn subject alt name, but only the kubelets know, the apiserver is clueless
|
||||
// alternateDNS = append(alternateDNS, "kubernetes.default.svc.CLUSTER.DNS.NAME")
|
||||
if crypto.ShouldGenSelfSignedCerts(options.TLSCertFile, options.TLSPrivateKeyFile) {
|
||||
if !crypto.FoundCertOrKey(options.TLSCertFile, options.TLSPrivateKeyFile) {
|
||||
if err := crypto.GenerateSelfSignedCert(s.ClusterIP.String(), options.TLSCertFile, options.TLSPrivateKeyFile, alternateIPs, alternateDNS); err != nil {
|
||||
glog.Errorf("Unable to generate self signed cert: %v", err)
|
||||
} else {
|
||||
|
@ -17,9 +17,16 @@ limitations under the License.
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
@ -38,3 +45,94 @@ func ParseCertificateRequestObject(obj *certificates.CertificateSigningRequest)
|
||||
}
|
||||
return csr, nil
|
||||
}
|
||||
|
||||
// GeneratePrivateKey returns PEM data containing a generated ECDSA private key
|
||||
func GeneratePrivateKey() ([]byte, error) {
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
derBytes, err := x509.MarshalECPrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKeyPemBlock := &pem.Block{
|
||||
Type: "EC PRIVATE KEY",
|
||||
Bytes: derBytes,
|
||||
}
|
||||
return pem.EncodeToMemory(privateKeyPemBlock), nil
|
||||
}
|
||||
|
||||
// ParsePrivateKey returns a private key parsed from a PEM block in the supplied data.
|
||||
// Recognizes PEM blocks for "EC PRIVATE KEY" and "RSA PRIVATE KEY"
|
||||
func ParsePrivateKey(keyData []byte) (interface{}, error) {
|
||||
for {
|
||||
var privateKeyPemBlock *pem.Block
|
||||
privateKeyPemBlock, keyData = pem.Decode(keyData)
|
||||
if privateKeyPemBlock == nil {
|
||||
// we read all the PEM blocks and didn't recognize one
|
||||
return nil, fmt.Errorf("no private key PEM block found")
|
||||
}
|
||||
|
||||
switch privateKeyPemBlock.Type {
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(privateKeyPemBlock.Bytes)
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(privateKeyPemBlock.Bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewCertificateRequest generates a PEM-encoded CSR using the supplied private key, subject, and SANs.
|
||||
// privateKey must be a *ecdsa.PrivateKey or *rsa.PrivateKey.
|
||||
func NewCertificateRequest(privateKey interface{}, subject *pkix.Name, dnsSANs []string, ipSANs []net.IP) (csr []byte, err error) {
|
||||
var sigType x509.SignatureAlgorithm
|
||||
|
||||
switch privateKey := privateKey.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
switch privateKey.Curve {
|
||||
case elliptic.P224(), elliptic.P256():
|
||||
sigType = x509.ECDSAWithSHA256
|
||||
case elliptic.P384():
|
||||
sigType = x509.ECDSAWithSHA384
|
||||
case elliptic.P521():
|
||||
sigType = x509.ECDSAWithSHA512
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown elliptic curve: %v", privateKey.Curve)
|
||||
}
|
||||
case *rsa.PrivateKey:
|
||||
keySize := privateKey.N.BitLen()
|
||||
switch {
|
||||
case keySize >= 4096:
|
||||
sigType = x509.SHA512WithRSA
|
||||
case keySize >= 3072:
|
||||
sigType = x509.SHA384WithRSA
|
||||
default:
|
||||
sigType = x509.SHA256WithRSA
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported key type: %T", privateKey)
|
||||
}
|
||||
|
||||
template := &x509.CertificateRequest{
|
||||
Subject: *subject,
|
||||
SignatureAlgorithm: sigType,
|
||||
DNSNames: dnsSANs,
|
||||
IPAddresses: ipSANs,
|
||||
}
|
||||
|
||||
csr, err = x509.CreateCertificateRequest(cryptorand.Reader, template, privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
csrPemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csr,
|
||||
}
|
||||
|
||||
return pem.EncodeToMemory(csrPemBlock), nil
|
||||
}
|
||||
|
46
pkg/util/certificates/csr_test.go
Normal file
46
pkg/util/certificates/csr_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
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 certificates
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewCertificateRequest(t *testing.T) {
|
||||
keyFile := "testdata/dontUseThisKey.pem"
|
||||
subject := &pkix.Name{
|
||||
CommonName: "kube-worker",
|
||||
}
|
||||
dnsSANs := []string{"localhost"}
|
||||
ipSANs := []net.IP{net.ParseIP("127.0.0.1")}
|
||||
|
||||
keyData, err := ioutil.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key, err := ParsePrivateKey(keyData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = NewCertificateRequest(key, subject, dnsSANs, ipSANs)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
6
pkg/util/certificates/testdata/dontUseThisKey.pem
vendored
Normal file
6
pkg/util/certificates/testdata/dontUseThisKey.pem
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDAPEbSXwyDfWf0+61Oofd7aHkmdX69mrzD2Xb1CHF5syfsoRIhnG0dJ
|
||||
ozBulPZCDDWgBwYFK4EEACKhZANiAATjlMJAtKhEPqU/i7MsrgKcK/RmXHC6He7W
|
||||
0p69+9qFXg2raJ9zvvbKxkiu2ELOYRDAz0utcFTBOIgoUJEzBVmsjZQ7dvFa1BKP
|
||||
Ym7MFAKG3O2espBqXn+audgdHGh5B0I=
|
||||
-----END EC PRIVATE KEY-----
|
@ -33,14 +33,14 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ShouldGenSelfSignedCerts returns false if the certificate or key files already exists,
|
||||
// otherwise returns true.
|
||||
func ShouldGenSelfSignedCerts(certPath, keyPath string) bool {
|
||||
// FoundCertOrKey returns true if the certificate or key files already exists,
|
||||
// otherwise returns false.
|
||||
func FoundCertOrKey(certPath, keyPath string) bool {
|
||||
if canReadFile(certPath) || canReadFile(keyPath) {
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
// If the file represented by path exists and
|
||||
@ -109,24 +109,46 @@ func GenerateSelfSignedCert(host, certPath, keyPath string, alternateIPs []net.I
|
||||
}
|
||||
|
||||
// Write cert
|
||||
if err := os.MkdirAll(filepath.Dir(certPath), os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(certPath, certBuffer.Bytes(), os.FileMode(0644)); err != nil {
|
||||
if err := WriteCertToPath(certPath, certBuffer.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write key
|
||||
if err := os.MkdirAll(filepath.Dir(keyPath), os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(keyPath, keyBuffer.Bytes(), os.FileMode(0600)); err != nil {
|
||||
if err := WriteKeyToPath(keyPath, keyBuffer.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteCertToPath writes the pem-encoded certificate data to certPath.
|
||||
// The certificate file will be created with file mode 0644.
|
||||
// If the certificate file already exists, it will be overwritten.
|
||||
// The parent directory of the certPath will be created as needed with file mode 0755.
|
||||
func WriteCertToPath(certPath string, data []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(certPath), os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(certPath, data, os.FileMode(0644)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteKeyToPath writes the pem-encoded key data to keyPath.
|
||||
// The key file will be created with file mode 0600.
|
||||
// If the key file already exists, it will be overwritten.
|
||||
// The parent directory of the keyPath will be created as needed with file mode 0755.
|
||||
func WriteKeyToPath(keyPath string, data []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(keyPath), os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(keyPath, data, os.FileMode(0600)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CertPoolFromFile returns an x509.CertPool containing the certificates in the given PEM-encoded file.
|
||||
// Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates
|
||||
func CertPoolFromFile(filename string) (*x509.CertPool, error) {
|
||||
|
Loading…
Reference in New Issue
Block a user