2019-01-12 04:58:27 +00:00
/ *
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 bootstrap
import (
"context"
2019-09-27 21:51:53 +00:00
"crypto"
2019-01-12 04:58:27 +00:00
"crypto/sha512"
2019-09-27 21:51:53 +00:00
"crypto/x509"
2019-01-12 04:58:27 +00:00
"crypto/x509/pkix"
"encoding/base64"
"errors"
"fmt"
"os"
"path/filepath"
"time"
2020-08-10 17:43:49 +00:00
"k8s.io/klog/v2"
2019-01-12 04:58:27 +00:00
2020-08-10 17:43:49 +00:00
certificatesv1 "k8s.io/api/certificates/v1"
2019-01-12 04:58:27 +00:00
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
2020-08-10 17:43:49 +00:00
clientset "k8s.io/client-go/kubernetes"
2019-01-12 04:58:27 +00:00
"k8s.io/client-go/kubernetes/scheme"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/transport"
certutil "k8s.io/client-go/util/cert"
"k8s.io/client-go/util/certificate"
"k8s.io/client-go/util/certificate/csr"
2019-04-07 17:07:55 +00:00
"k8s.io/client-go/util/keyutil"
2019-01-12 04:58:27 +00:00
)
const tmpPrivateKeyFile = "kubelet-client.key.tmp"
2019-04-07 17:07:55 +00:00
// LoadClientConfig tries to load the appropriate client config for retrieving certs and for use by users.
// If bootstrapPath is empty, only kubeconfigPath is checked. If bootstrap path is set and the contents
// of kubeconfigPath are valid, both certConfig and userConfig will point to that file. Otherwise the
// kubeconfigPath on disk is populated based on bootstrapPath but pointing to the location of the client cert
// in certDir. This preserves the historical behavior of bootstrapping where on subsequent restarts the
// most recent client cert is used to request new client certs instead of the initial token.
func LoadClientConfig ( kubeconfigPath , bootstrapPath , certDir string ) ( certConfig , userConfig * restclient . Config , err error ) {
if len ( bootstrapPath ) == 0 {
clientConfig , err := loadRESTClientConfig ( kubeconfigPath )
if err != nil {
return nil , nil , fmt . Errorf ( "unable to load kubeconfig: %v" , err )
}
2021-03-18 22:40:29 +00:00
klog . V ( 2 ) . InfoS ( "No bootstrapping requested, will use kubeconfig" )
2019-04-07 17:07:55 +00:00
return clientConfig , restclient . CopyConfig ( clientConfig ) , nil
}
store , err := certificate . NewFileStore ( "kubelet-client" , certDir , certDir , "" , "" )
if err != nil {
return nil , nil , fmt . Errorf ( "unable to build bootstrap cert store" )
}
ok , err := isClientConfigStillValid ( kubeconfigPath )
if err != nil {
return nil , nil , err
}
// use the current client config
if ok {
clientConfig , err := loadRESTClientConfig ( kubeconfigPath )
if err != nil {
return nil , nil , fmt . Errorf ( "unable to load kubeconfig: %v" , err )
}
2021-03-18 22:40:29 +00:00
klog . V ( 2 ) . InfoS ( "Current kubeconfig file contents are still valid, no bootstrap necessary" )
2019-04-07 17:07:55 +00:00
return clientConfig , restclient . CopyConfig ( clientConfig ) , nil
}
bootstrapClientConfig , err := loadRESTClientConfig ( bootstrapPath )
if err != nil {
return nil , nil , fmt . Errorf ( "unable to load bootstrap kubeconfig: %v" , err )
}
clientConfig := restclient . AnonymousClientConfig ( bootstrapClientConfig )
pemPath := store . CurrentPath ( )
clientConfig . KeyFile = pemPath
clientConfig . CertFile = pemPath
if err := writeKubeconfigFromBootstrapping ( clientConfig , kubeconfigPath , pemPath ) ; err != nil {
return nil , nil , err
}
2021-03-18 22:40:29 +00:00
klog . V ( 2 ) . InfoS ( "Use the bootstrap credentials to request a cert, and set kubeconfig to point to the certificate dir" )
2019-04-07 17:07:55 +00:00
return bootstrapClientConfig , clientConfig , nil
}
2019-01-12 04:58:27 +00:00
// LoadClientCert 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.
2020-08-10 17:43:49 +00:00
func LoadClientCert ( ctx context . Context , kubeconfigPath , bootstrapPath , certDir string , nodeName types . NodeName ) error {
2019-01-12 04:58:27 +00:00
// Short-circuit if the kubeconfig file exists and is valid.
2019-04-07 17:07:55 +00:00
ok , err := isClientConfigStillValid ( kubeconfigPath )
2019-01-12 04:58:27 +00:00
if err != nil {
return err
}
if ok {
2021-03-18 22:40:29 +00:00
klog . V ( 2 ) . InfoS ( "Kubeconfig exists and is valid, skipping bootstrap" , "path" , kubeconfigPath )
2019-01-12 04:58:27 +00:00
return nil
}
2021-03-18 22:40:29 +00:00
klog . V ( 2 ) . InfoS ( "Using bootstrap kubeconfig to generate TLS client cert, key and kubeconfig file" )
2019-01-12 04:58:27 +00:00
bootstrapClientConfig , err := loadRESTClientConfig ( bootstrapPath )
if err != nil {
return fmt . Errorf ( "unable to load bootstrap kubeconfig: %v" , err )
}
2020-08-10 17:43:49 +00:00
bootstrapClient , err := clientset . NewForConfig ( bootstrapClientConfig )
2019-01-12 04:58:27 +00:00
if err != nil {
return fmt . Errorf ( "unable to create certificates signing request client: %v" , err )
}
store , err := certificate . NewFileStore ( "kubelet-client" , certDir , certDir , "" , "" )
if err != nil {
return fmt . Errorf ( "unable to build bootstrap cert store" )
}
var keyData [ ] byte
if cert , err := store . Current ( ) ; err == nil {
if cert . PrivateKey != nil {
2019-04-07 17:07:55 +00:00
keyData , err = keyutil . MarshalPrivateKeyToPEM ( cert . PrivateKey )
2019-01-12 04:58:27 +00:00
if err != nil {
keyData = nil
}
}
}
// Cache the private key in a separate file until CSR succeeds. This has to
// be a separate file because store.CurrentPath() points to a symlink
// managed by the store.
privKeyPath := filepath . Join ( certDir , tmpPrivateKeyFile )
if ! verifyKeyData ( keyData ) {
2021-03-18 22:40:29 +00:00
klog . V ( 2 ) . InfoS ( "No valid private key and/or certificate found, reusing existing private key or creating a new one" )
2019-01-12 04:58:27 +00:00
// Note: always call LoadOrGenerateKeyFile so that private key is
// reused on next startup if CSR request fails.
2019-04-07 17:07:55 +00:00
keyData , _ , err = keyutil . LoadOrGenerateKeyFile ( privKeyPath )
2019-01-12 04:58:27 +00:00
if err != nil {
return err
}
}
2020-08-10 17:43:49 +00:00
if err := waitForServer ( ctx , * bootstrapClientConfig , 1 * time . Minute ) ; err != nil {
2021-03-18 22:40:29 +00:00
klog . InfoS ( "Error waiting for apiserver to come up" , "err" , err )
2019-01-12 04:58:27 +00:00
}
2020-08-10 17:43:49 +00:00
certData , err := requestNodeCertificate ( ctx , bootstrapClient , keyData , nodeName )
2019-01-12 04:58:27 +00:00
if err != nil {
return err
}
if _ , err := store . Update ( certData , keyData ) ; err != nil {
return err
}
if err := os . Remove ( privKeyPath ) ; err != nil && ! os . IsNotExist ( err ) {
2021-03-18 22:40:29 +00:00
klog . V ( 2 ) . InfoS ( "Failed cleaning up private key file" , "path" , privKeyPath , "err" , err )
2019-01-12 04:58:27 +00:00
}
2019-04-07 17:07:55 +00:00
return writeKubeconfigFromBootstrapping ( bootstrapClientConfig , kubeconfigPath , store . CurrentPath ( ) )
}
2019-01-12 04:58:27 +00:00
2019-04-07 17:07:55 +00:00
func writeKubeconfigFromBootstrapping ( bootstrapClientConfig * restclient . Config , kubeconfigPath , pemPath string ) error {
2019-01-12 04:58:27 +00:00
// 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 : pemPath ,
ClientKey : pemPath ,
} } ,
// 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
return clientcmd . WriteToFile ( kubeconfigData , kubeconfigPath )
}
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 ( )
}
2019-04-07 17:07:55 +00:00
// isClientConfigStillValid checks the provided kubeconfig to see if it has a valid
2019-01-12 04:58:27 +00:00
// client certificate. It returns true if the kubeconfig is valid, or an error if bootstrapping
// should stop immediately.
2019-04-07 17:07:55 +00:00
func isClientConfigStillValid ( kubeconfigPath string ) ( bool , error ) {
2019-01-12 04:58:27 +00:00
_ , err := os . Stat ( kubeconfigPath )
if os . IsNotExist ( err ) {
return false , nil
}
if err != nil {
return false , fmt . Errorf ( "error reading existing bootstrap kubeconfig %s: %v" , kubeconfigPath , err )
}
bootstrapClientConfig , err := loadRESTClientConfig ( kubeconfigPath )
if err != nil {
2021-03-18 22:40:29 +00:00
utilruntime . HandleError ( fmt . Errorf ( "unable to read existing bootstrap client config from %s: %v" , kubeconfigPath , err ) )
2019-01-12 04:58:27 +00:00
return false , nil
}
transportConfig , err := bootstrapClientConfig . TransportConfig ( )
if err != nil {
2021-03-18 22:40:29 +00:00
utilruntime . HandleError ( fmt . Errorf ( "unable to load transport configuration from existing bootstrap client config read from %s: %v" , kubeconfigPath , err ) )
2019-01-12 04:58:27 +00:00
return false , nil
}
// has side effect of populating transport config data fields
if _ , err := transport . TLSConfigFor ( transportConfig ) ; err != nil {
2021-03-18 22:40:29 +00:00
utilruntime . HandleError ( fmt . Errorf ( "unable to load TLS configuration from existing bootstrap client config read from %s: %v" , kubeconfigPath , err ) )
2019-01-12 04:58:27 +00:00
return false , nil
}
certs , err := certutil . ParseCertsPEM ( transportConfig . TLS . CertData )
if err != nil {
2021-03-18 22:40:29 +00:00
utilruntime . HandleError ( fmt . Errorf ( "unable to load TLS certificates from existing bootstrap client config read from %s: %v" , kubeconfigPath , err ) )
2019-01-12 04:58:27 +00:00
return false , nil
}
if len ( certs ) == 0 {
2021-03-18 22:40:29 +00:00
utilruntime . HandleError ( fmt . Errorf ( "unable to read TLS certificates from existing bootstrap client config read from %s: %v" , kubeconfigPath , err ) )
2019-01-12 04:58:27 +00:00
return false , nil
}
now := time . Now ( )
for _ , cert := range certs {
if now . After ( cert . NotAfter ) {
2021-03-18 22:40:29 +00:00
utilruntime . HandleError ( fmt . Errorf ( "part of the existing bootstrap client certificate in %s is expired: %v" , kubeconfigPath , cert . NotAfter ) )
2019-01-12 04:58:27 +00:00
return false , nil
}
}
return true , nil
}
// verifyKeyData returns true if the provided data appears to be a valid private key.
func verifyKeyData ( data [ ] byte ) bool {
if len ( data ) == 0 {
return false
}
2019-04-07 17:07:55 +00:00
_ , err := keyutil . ParsePrivateKeyPEM ( data )
2019-01-12 04:58:27 +00:00
return err == nil
}
2020-08-10 17:43:49 +00:00
func waitForServer ( ctx context . Context , cfg restclient . Config , deadline time . Duration ) error {
2019-08-30 18:33:25 +00:00
cfg . NegotiatedSerializer = scheme . Codecs . WithoutConversion ( )
2019-01-12 04:58:27 +00:00
cfg . Timeout = 1 * time . Second
cli , err := restclient . UnversionedRESTClientFor ( & cfg )
if err != nil {
return fmt . Errorf ( "couldn't create client: %v" , err )
}
2020-08-10 17:43:49 +00:00
ctx , cancel := context . WithTimeout ( ctx , deadline )
2019-01-12 04:58:27 +00:00
defer cancel ( )
var connected bool
wait . JitterUntil ( func ( ) {
2020-08-10 17:43:49 +00:00
if _ , err := cli . Get ( ) . AbsPath ( "/healthz" ) . Do ( ctx ) . Raw ( ) ; err != nil {
2021-03-18 22:40:29 +00:00
klog . InfoS ( "Failed to connect to apiserver" , "err" , err )
2019-01-12 04:58:27 +00:00
return
}
cancel ( )
connected = true
} , 2 * time . Second , 0.2 , true , ctx . Done ( ) )
if ! connected {
return errors . New ( "timed out waiting to connect to apiserver" )
}
return nil
}
// requestNodeCertificate will create a certificate signing request for a node
// (Organization and CommonName for the CSR will be set as expected for node
// certificates) 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. This is intended for use on nodes (kubelet and
// kubeadm).
2020-08-10 17:43:49 +00:00
func requestNodeCertificate ( ctx context . Context , client clientset . Interface , privateKeyData [ ] byte , nodeName types . NodeName ) ( certData [ ] byte , err error ) {
2019-01-12 04:58:27 +00:00
subject := & pkix . Name {
Organization : [ ] string { "system:nodes" } ,
CommonName : "system:node:" + string ( nodeName ) ,
}
2019-04-07 17:07:55 +00:00
privateKey , err := keyutil . ParsePrivateKeyPEM ( privateKeyData )
2019-01-12 04:58:27 +00:00
if err != nil {
return nil , fmt . Errorf ( "invalid private key for certificate request: %v" , err )
}
csrData , err := certutil . MakeCSR ( privateKey , subject , nil , nil )
if err != nil {
return nil , fmt . Errorf ( "unable to generate certificate request: %v" , err )
}
2020-08-10 17:43:49 +00:00
usages := [ ] certificatesv1 . KeyUsage {
certificatesv1 . UsageDigitalSignature ,
certificatesv1 . UsageKeyEncipherment ,
certificatesv1 . UsageClientAuth ,
2019-01-12 04:58:27 +00:00
}
2019-09-27 21:51:53 +00:00
// The Signer interface contains the Public() method to get the public key.
signer , ok := privateKey . ( crypto . Signer )
if ! ok {
return nil , fmt . Errorf ( "private key does not implement crypto.Signer" )
}
name , err := digestedName ( signer . Public ( ) , subject , usages )
if err != nil {
return nil , err
}
2020-08-10 17:43:49 +00:00
reqName , reqUID , err := csr . RequestCertificate ( client , csrData , name , certificatesv1 . KubeAPIServerClientKubeletSignerName , usages , privateKey )
2019-01-12 04:58:27 +00:00
if err != nil {
return nil , err
}
2019-08-30 18:33:25 +00:00
2020-08-10 17:43:49 +00:00
ctx , cancel := context . WithTimeout ( ctx , 3600 * time . Second )
2019-08-30 18:33:25 +00:00
defer cancel ( )
2021-03-18 22:40:29 +00:00
klog . V ( 2 ) . InfoS ( "Waiting for client certificate to be issued" )
2020-08-10 17:43:49 +00:00
return csr . WaitForCertificate ( ctx , client , reqName , reqUID )
2019-01-12 04:58:27 +00:00
}
// This digest should include all the relevant pieces of the CSR we care about.
2019-12-12 01:27:03 +00:00
// We can't directly hash the serialized CSR because of random padding that we
2019-01-12 04:58:27 +00:00
// regenerate every loop and we include usages which are not contained in the
// CSR. This needs to be kept up to date as we add new fields to the node
// certificates and with ensureCompatible.
2020-08-10 17:43:49 +00:00
func digestedName ( publicKey interface { } , subject * pkix . Name , usages [ ] certificatesv1 . KeyUsage ) ( string , error ) {
2019-01-12 04:58:27 +00:00
hash := sha512 . New512_256 ( )
// Here we make sure two different inputs can't write the same stream
// to the hash. This delimiter is not in the base64.URLEncoding
// alphabet so there is no way to have spill over collisions. Without
// it 'CN:foo,ORG:bar' hashes to the same value as 'CN:foob,ORG:ar'
const delimiter = '|'
encode := base64 . RawURLEncoding . EncodeToString
write := func ( data [ ] byte ) {
hash . Write ( [ ] byte ( encode ( data ) ) )
hash . Write ( [ ] byte { delimiter } )
}
2019-09-27 21:51:53 +00:00
publicKeyData , err := x509 . MarshalPKIXPublicKey ( publicKey )
if err != nil {
return "" , err
}
write ( publicKeyData )
2019-01-12 04:58:27 +00:00
write ( [ ] byte ( subject . CommonName ) )
for _ , v := range subject . Organization {
write ( [ ] byte ( v ) )
}
for _ , v := range usages {
write ( [ ] byte ( v ) )
}
2019-09-27 21:51:53 +00:00
return fmt . Sprintf ( "node-csr-%s" , encode ( hash . Sum ( nil ) ) ) , nil
2019-01-12 04:58:27 +00:00
}