2016-11-08 19:27:16 +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 token
import (
2017-07-24 21:34:36 +00:00
"bytes"
"crypto/x509"
"encoding/pem"
2017-03-04 09:17:52 +00:00
"fmt"
"sync"
2016-11-08 19:27:16 +00:00
2017-06-22 18:24:23 +00:00
"k8s.io/api/core/v1"
2017-03-04 09:17:52 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
2018-01-17 08:22:37 +00:00
bootstrapapi "k8s.io/client-go/tools/bootstrap/token/api"
2017-03-04 09:17:52 +00:00
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig"
2017-07-24 21:34:36 +00:00
"k8s.io/kubernetes/cmd/kubeadm/app/util/pubkeypin"
2017-03-04 09:17:52 +00:00
tokenutil "k8s.io/kubernetes/cmd/kubeadm/app/util/token"
"k8s.io/kubernetes/pkg/controller/bootstrap"
2016-11-08 19:27:16 +00:00
)
2017-09-10 20:23:14 +00:00
// BootstrapUser defines bootstrap user name
2017-03-04 09:17:52 +00:00
const BootstrapUser = "token-bootstrap-client"
// RetrieveValidatedClusterInfo connects to the API Server and tries to fetch the cluster-info ConfigMap
2017-07-24 21:34:36 +00:00
// It then makes sure it can trust the API Server by looking at the JWS-signed tokens and (if rootCAPubKeys is not empty)
// validating the cluster CA against a set of pinned public keys
func RetrieveValidatedClusterInfo ( discoveryToken string , tokenAPIServers , rootCAPubKeys [ ] string ) ( * clientcmdapi . Cluster , error ) {
2017-09-10 20:23:14 +00:00
tokenID , tokenSecret , err := tokenutil . ParseToken ( discoveryToken )
2017-03-04 09:17:52 +00:00
if err != nil {
return nil , err
2016-11-08 19:27:16 +00:00
}
2017-03-04 09:17:52 +00:00
2017-07-24 21:34:36 +00:00
// Load the cfg.DiscoveryTokenCACertHashes into a pubkeypin.Set
pubKeyPins := pubkeypin . NewSet ( )
err = pubKeyPins . Allow ( rootCAPubKeys ... )
if err != nil {
return nil , err
}
2017-03-04 09:17:52 +00:00
// The function below runs for every endpoint, and all endpoints races with each other.
// The endpoint that wins the race and completes the task first gets its kubeconfig returned below
baseKubeConfig := runForEndpointsAndReturnFirst ( tokenAPIServers , func ( endpoint string ) ( * clientcmdapi . Config , error ) {
2017-07-24 21:34:36 +00:00
insecureBootstrapConfig := buildInsecureBootstrapKubeConfig ( endpoint )
clusterName := insecureBootstrapConfig . Contexts [ insecureBootstrapConfig . CurrentContext ] . Cluster
2017-03-04 09:17:52 +00:00
2017-09-10 20:23:14 +00:00
insecureClient , err := kubeconfigutil . ToClientSet ( insecureBootstrapConfig )
2017-03-04 09:17:52 +00:00
if err != nil {
return nil , err
2016-11-08 19:27:16 +00:00
}
2017-03-04 09:17:52 +00:00
2017-07-24 21:34:36 +00:00
fmt . Printf ( "[discovery] Created cluster-info discovery client, requesting info from %q\n" , insecureBootstrapConfig . Clusters [ clusterName ] . Server )
2017-03-04 09:17:52 +00:00
2017-07-24 21:34:36 +00:00
// Make an initial insecure connection to get the cluster-info ConfigMap
var insecureClusterInfo * v1 . ConfigMap
2017-07-09 21:26:23 +00:00
wait . PollImmediateInfinite ( constants . DiscoveryRetryInterval , func ( ) ( bool , error ) {
2017-03-04 09:17:52 +00:00
var err error
2017-07-24 21:34:36 +00:00
insecureClusterInfo , err = insecureClient . CoreV1 ( ) . ConfigMaps ( metav1 . NamespacePublic ) . Get ( bootstrapapi . ConfigMapClusterInfo , metav1 . GetOptions { } )
2017-03-04 09:17:52 +00:00
if err != nil {
fmt . Printf ( "[discovery] Failed to request cluster info, will try again: [%s]\n" , err )
return false , nil
}
return true , nil
} )
2017-07-24 21:34:36 +00:00
// Validate the MAC on the kubeconfig from the ConfigMap and load it
insecureKubeconfigString , ok := insecureClusterInfo . Data [ bootstrapapi . KubeConfigKey ]
if ! ok || len ( insecureKubeconfigString ) == 0 {
2017-03-04 09:17:52 +00:00
return nil , fmt . Errorf ( "there is no %s key in the %s ConfigMap. This API Server isn't set up for token bootstrapping, can't connect" , bootstrapapi . KubeConfigKey , bootstrapapi . ConfigMapClusterInfo )
}
2017-09-10 20:23:14 +00:00
detachedJWSToken , ok := insecureClusterInfo . Data [ bootstrapapi . JWSSignatureKeyPrefix + tokenID ]
2017-03-04 09:17:52 +00:00
if ! ok || len ( detachedJWSToken ) == 0 {
2018-01-07 07:16:58 +00:00
return nil , fmt . Errorf ( "token id %q is invalid for this cluster or it has expired. Use \"kubeadm token create\" on the master node to creating a new valid token" , tokenID )
2017-03-04 09:17:52 +00:00
}
2017-09-10 20:23:14 +00:00
if ! bootstrap . DetachedTokenIsValid ( detachedJWSToken , insecureKubeconfigString , tokenID , tokenSecret ) {
2017-03-04 09:17:52 +00:00
return nil , fmt . Errorf ( "failed to verify JWS signature of received cluster info object, can't trust this API Server" )
}
2017-07-24 21:34:36 +00:00
insecureKubeconfigBytes := [ ] byte ( insecureKubeconfigString )
insecureConfig , err := clientcmd . Load ( insecureKubeconfigBytes )
if err != nil {
return nil , fmt . Errorf ( "couldn't parse the kubeconfig file in the %s configmap: %v" , bootstrapapi . ConfigMapClusterInfo , err )
}
// If no TLS root CA pinning was specified, we're done
if pubKeyPins . Empty ( ) {
fmt . Printf ( "[discovery] Cluster info signature and contents are valid and no TLS pinning was specified, will use API Server %q\n" , endpoint )
return insecureConfig , nil
}
// Load the cluster CA from the Config
if len ( insecureConfig . Clusters ) != 1 {
return nil , fmt . Errorf ( "expected the kubeconfig file in the %s configmap to have a single cluster, but it had %d" , bootstrapapi . ConfigMapClusterInfo , len ( insecureConfig . Clusters ) )
}
var clusterCABytes [ ] byte
for _ , cluster := range insecureConfig . Clusters {
clusterCABytes = cluster . CertificateAuthorityData
}
clusterCA , err := parsePEMCert ( clusterCABytes )
if err != nil {
return nil , fmt . Errorf ( "failed to parse cluster CA from the %s configmap: %v" , bootstrapapi . ConfigMapClusterInfo , err )
}
// Validate the cluster CA public key against the pinned set
err = pubKeyPins . Check ( clusterCA )
if err != nil {
return nil , fmt . Errorf ( "cluster CA found in %s configmap is invalid: %v" , bootstrapapi . ConfigMapClusterInfo , err )
}
// Now that we know the proported cluster CA, connect back a second time validating with that CA
secureBootstrapConfig := buildSecureBootstrapKubeConfig ( endpoint , clusterCABytes )
2017-09-10 20:23:14 +00:00
secureClient , err := kubeconfigutil . ToClientSet ( secureBootstrapConfig )
2017-07-24 21:34:36 +00:00
if err != nil {
return nil , err
}
fmt . Printf ( "[discovery] Requesting info from %q again to validate TLS against the pinned public key\n" , insecureBootstrapConfig . Clusters [ clusterName ] . Server )
var secureClusterInfo * v1 . ConfigMap
wait . PollImmediateInfinite ( constants . DiscoveryRetryInterval , func ( ) ( bool , error ) {
var err error
secureClusterInfo , err = secureClient . CoreV1 ( ) . ConfigMaps ( metav1 . NamespacePublic ) . Get ( bootstrapapi . ConfigMapClusterInfo , metav1 . GetOptions { } )
if err != nil {
fmt . Printf ( "[discovery] Failed to request cluster info, will try again: [%s]\n" , err )
return false , nil
}
return true , nil
} )
2017-03-04 09:17:52 +00:00
2017-07-24 21:34:36 +00:00
// Pull the kubeconfig from the securely-obtained ConfigMap and validate that it's the same as what we found the first time
secureKubeconfigBytes := [ ] byte ( secureClusterInfo . Data [ bootstrapapi . KubeConfigKey ] )
if ! bytes . Equal ( secureKubeconfigBytes , insecureKubeconfigBytes ) {
return nil , fmt . Errorf ( "the second kubeconfig from the %s configmap (using validated TLS) was different from the first" , bootstrapapi . ConfigMapClusterInfo )
}
secureKubeconfig , err := clientcmd . Load ( secureKubeconfigBytes )
2017-03-04 09:17:52 +00:00
if err != nil {
return nil , fmt . Errorf ( "couldn't parse the kubeconfig file in the %s configmap: %v" , bootstrapapi . ConfigMapClusterInfo , err )
}
2017-07-24 21:34:36 +00:00
fmt . Printf ( "[discovery] Cluster info signature and contents are valid and TLS certificate validates against pinned roots, will use API Server %q\n" , endpoint )
return secureKubeconfig , nil
2017-03-04 09:17:52 +00:00
} )
return kubeconfigutil . GetClusterFromKubeConfig ( baseKubeConfig ) , nil
}
// buildInsecureBootstrapKubeConfig makes a KubeConfig object that connects insecurely to the API Server for bootstrapping purposes
func buildInsecureBootstrapKubeConfig ( endpoint string ) * clientcmdapi . Config {
masterEndpoint := fmt . Sprintf ( "https://%s" , endpoint )
clusterName := "kubernetes"
bootstrapConfig := kubeconfigutil . CreateBasic ( masterEndpoint , clusterName , BootstrapUser , [ ] byte { } )
bootstrapConfig . Clusters [ clusterName ] . InsecureSkipTLSVerify = true
return bootstrapConfig
}
2017-07-24 21:34:36 +00:00
// buildSecureBootstrapKubeConfig makes a KubeConfig object that connects securely to the API Server for bootstrapping purposes (validating with the specified CA)
func buildSecureBootstrapKubeConfig ( endpoint string , caCert [ ] byte ) * clientcmdapi . Config {
masterEndpoint := fmt . Sprintf ( "https://%s" , endpoint )
bootstrapConfig := kubeconfigutil . CreateBasic ( masterEndpoint , "kubernetes" , BootstrapUser , caCert )
return bootstrapConfig
}
2017-03-04 09:17:52 +00:00
// runForEndpointsAndReturnFirst loops the endpoints slice and let's the endpoints race for connecting to the master
func runForEndpointsAndReturnFirst ( endpoints [ ] string , fetchKubeConfigFunc func ( string ) ( * clientcmdapi . Config , error ) ) * clientcmdapi . Config {
stopChan := make ( chan struct { } )
var resultingKubeConfig * clientcmdapi . Config
var once sync . Once
var wg sync . WaitGroup
for _ , endpoint := range endpoints {
wg . Add ( 1 )
go func ( apiEndpoint string ) {
defer wg . Done ( )
wait . Until ( func ( ) {
fmt . Printf ( "[discovery] Trying to connect to API Server %q\n" , apiEndpoint )
cfg , err := fetchKubeConfigFunc ( apiEndpoint )
if err != nil {
fmt . Printf ( "[discovery] Failed to connect to API Server %q: %v\n" , apiEndpoint , err )
return
}
fmt . Printf ( "[discovery] Successfully established connection with API Server %q\n" , apiEndpoint )
// connection established, stop all wait threads
once . Do ( func ( ) {
close ( stopChan )
resultingKubeConfig = cfg
} )
} , constants . DiscoveryRetryInterval , stopChan )
} ( endpoint )
2016-11-08 19:27:16 +00:00
}
2017-03-04 09:17:52 +00:00
wg . Wait ( )
return resultingKubeConfig
2016-11-08 19:27:16 +00:00
}
2017-07-24 21:34:36 +00:00
// parsePEMCert decodes a PEM-formatted certificate and returns it as an x509.Certificate
func parsePEMCert ( certData [ ] byte ) ( * x509 . Certificate , error ) {
pemBlock , trailingData := pem . Decode ( certData )
if pemBlock == nil {
return nil , fmt . Errorf ( "invalid PEM data" )
}
if len ( trailingData ) != 0 {
return nil , fmt . Errorf ( "trailing data after first PEM block" )
}
return x509 . ParseCertificate ( pemBlock . Bytes )
}