Kubelet API Certificate

A while back options to permit secure kube-apiserver to kubelet api was https://github.com/kubernetes/kops/pull/2831 using the server.cert and server.key as testing grouns. This PR formalizes the options and generates a client certificate on their behalf (note, the server{.cert,key} can no longer be used post 1.7 as the certificate usage is checked i.e. it's not using a client cert). The users now only need to add anonymousAuth: false to enable secure api to kubelet. I'd like to make this default to all new builds i'm not sure where to place it.

- updated the security.md to reflect the changes
- issue a new client kubelet-api certificate used to secure authorize comms between api and kubelet
- fixed any formatting issues i came across on the journey
This commit is contained in:
Rohith 2017-08-02 21:29:43 +01:00
parent 38608bd802
commit 2fb60b9b3d
6 changed files with 146 additions and 12 deletions

View File

@ -34,13 +34,25 @@ This stores the [config.json](https://docs.docker.com/engine/reference/commandli
All Pods running on your cluster have access to underlying instance IAM role. All Pods running on your cluster have access to underlying instance IAM role.
Currently permission scope is quite broad. See [iam_roles.md](iam_roles.md) for details and ways to mitigate that. Currently permission scope is quite broad. See [iam_roles.md](iam_roles.md) for details and ways to mitigate that.
## Kubernetes API ## Kubernetes API
(this section is a work in progress) (this section is a work in progress)
Kubernetes has a number of authentication mechanisms: Kubernetes has a number of authentication mechanisms:
## Kubelet API
By default AnonymousAuth on the kubelet is off and so communication between kube-apiserver and kubelet api is not authenticated. In order to switch on authentication;
```YAML
# In the cluster spec
spec:
kubelet:
anonymousAuth: false
```
**Note** on a existing cluster with 'anonymousAuth' unset you would need to first roll out the masters and then update the pools.
### API Bearer Token ### API Bearer Token
The API bearer token is a secret named 'admin'. The API bearer token is a secret named 'admin'.

View File

@ -198,3 +198,30 @@ func (c *NodeupModelContext) UsesCNI() bool {
} }
return true return true
} }
// UseSecureKubelet checks if the kubelet api should be protected by a client certificate. Note: the settings are be
// in one of three section, master specific kubelet, cluster wide kubelet or the InstanceGroup. Though arguably is
// doesn't make much sense to unset this on a per InstanceGroup level, but hey :)
func (c *NodeupModelContext) UseSecureKubelet() bool {
cluster := &c.Cluster.Spec // just to shorten the typing
group := &c.InstanceGroup.Spec
// @check if we have anything specific to master kubelet
if c.IsMaster {
if cluster.MasterKubelet != nil && cluster.MasterKubelet.AnonymousAuth != nil && *cluster.MasterKubelet.AnonymousAuth == true {
return true
}
}
// @check the default settings for master and kubelet
if cluster.Kubelet != nil && cluster.Kubelet.AnonymousAuth != nil && *cluster.Kubelet.AnonymousAuth == false {
return true
}
// @check on the InstanceGroup itself
if group.Kubelet != nil && group.Kubelet.AnonymousAuth != nil && *group.Kubelet.AnonymousAuth == false {
return true
}
return false
}

View File

@ -17,12 +17,16 @@ limitations under the License.
package model package model
import ( import (
"fmt"
"path/filepath"
"strconv" "strconv"
"github.com/golang/glog"
"k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/api/v1"
"k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/nodeup/nodetasks"
"github.com/golang/glog"
) )
// s is a helper that builds a *string from a string value // s is a helper that builds a *string from a string value
@ -35,6 +39,11 @@ func i64(v int64) *int64 {
return fi.Int64(v) return fi.Int64(v)
} }
// b returns a pointer to a boolean
func b(v bool) *bool {
return fi.Bool(v)
}
func getProxyEnvVars(proxies *kops.EgressProxySpec) []v1.EnvVar { func getProxyEnvVars(proxies *kops.EgressProxySpec) []v1.EnvVar {
if proxies == nil { if proxies == nil {
glog.V(8).Info("proxies is == nil, returning empty list") glog.V(8).Info("proxies is == nil, returning empty list")
@ -62,7 +71,54 @@ func getProxyEnvVars(proxies *kops.EgressProxySpec) []v1.EnvVar {
} }
} }
// b returns a pointer to a boolean // buildCertificateRequest retrieves the certificate from a keystore
func b(v bool) *bool { func buildCertificateRequest(c *fi.ModelBuilderContext, b *NodeupModelContext, name, path string) error {
return fi.Bool(v) cert, err := b.KeyStore.Cert(name)
if err != nil {
return err
}
serialized, err := cert.AsString()
if err != nil {
return err
}
location := filepath.Join(b.PathSrvKubernetes(), fmt.Sprintf("%s.pem", name))
if path != "" {
location = path
}
c.AddTask(&nodetasks.File{
Path: location,
Contents: fi.NewStringResource(serialized),
Type: nodetasks.FileType_File,
})
return nil
}
// buildPrivateKeyRequest retrieves a private key from the store
func buildPrivateKeyRequest(c *fi.ModelBuilderContext, b *NodeupModelContext, name, path string) error {
k, err := b.KeyStore.PrivateKey(name)
if err != nil {
return err
}
serialized, err := k.AsString()
if err != nil {
return err
}
location := filepath.Join(b.PathSrvKubernetes(), fmt.Sprintf("%s-key.pem", name))
if path != "" {
location = path
}
c.AddTask(&nodetasks.File{
Path: location,
Contents: fi.NewStringResource(serialized),
Type: nodetasks.FileType_File,
})
return nil
} }

View File

@ -41,7 +41,7 @@ type KubeAPIServerBuilder struct {
var _ fi.ModelBuilder = &KubeAPIServerBuilder{} var _ fi.ModelBuilder = &KubeAPIServerBuilder{}
// Build is responsible for generating the kubernetes api manifest // Build is responsible for generating the configuration for the kube-apiserver
func (b *KubeAPIServerBuilder) Build(c *fi.ModelBuilderContext) error { func (b *KubeAPIServerBuilder) Build(c *fi.ModelBuilderContext) error {
if !b.IsMaster { if !b.IsMaster {
return nil return nil
@ -71,6 +71,19 @@ func (b *KubeAPIServerBuilder) Build(c *fi.ModelBuilderContext) error {
c.AddTask(t) c.AddTask(t)
} }
// @check if we are using secure client certificates for kubelet and grab the certificates
{
if b.UseSecureKubelet() {
name := "kubelet-api"
if err := buildCertificateRequest(c, b.NodeupModelContext, name, ""); err != nil {
return err
}
if err := buildPrivateKeyRequest(c, b.NodeupModelContext, name, ""); err != nil {
return err
}
}
}
// Touch log file, so that docker doesn't create a directory instead // Touch log file, so that docker doesn't create a directory instead
{ {
t := &nodetasks.File{ t := &nodetasks.File{
@ -135,6 +148,7 @@ func (b *KubeAPIServerBuilder) writeAuthenticationConfig(c *fi.ModelBuilderConte
return fmt.Errorf("Unrecognized authentication config %v", b.Cluster.Spec.Authentication) return fmt.Errorf("Unrecognized authentication config %v", b.Cluster.Spec.Authentication)
} }
// buildPod is responsible for generating the kube-apiserver pod and thus manifest file
func (b *KubeAPIServerBuilder) buildPod() (*v1.Pod, error) { func (b *KubeAPIServerBuilder) buildPod() (*v1.Pod, error) {
kubeAPIServer := b.Cluster.Spec.KubeAPIServer kubeAPIServer := b.Cluster.Spec.KubeAPIServer
kubeAPIServer.ClientCAFile = filepath.Join(b.PathSrvKubernetes(), "ca.crt") kubeAPIServer.ClientCAFile = filepath.Join(b.PathSrvKubernetes(), "ca.crt")
@ -151,6 +165,14 @@ func (b *KubeAPIServerBuilder) buildPod() (*v1.Pod, error) {
kubeAPIServer.EtcdServersOverrides = []string{"/events#https://127.0.0.1:4002"} kubeAPIServer.EtcdServersOverrides = []string{"/events#https://127.0.0.1:4002"}
} }
// @check if we are using secure kubelet client certificates
if b.UseSecureKubelet() {
// @note we are making assumption we are using the one's created by the pki model, not custom defined ones
kubeAPIServer.KubeletClientCertificate = filepath.Join(b.PathSrvKubernetes(), "kubelet-api.pem")
kubeAPIServer.KubeletClientKey = filepath.Join(b.PathSrvKubernetes(), "kubelet-api-key.pem")
}
// build the kube-apiserver flags for the service
flags, err := flagbuilder.BuildFlags(b.Cluster.Spec.KubeAPIServer) flags, err := flagbuilder.BuildFlags(b.Cluster.Spec.KubeAPIServer)
if err != nil { if err != nil {
return nil, fmt.Errorf("error building kube-apiserver flags: %v", err) return nil, fmt.Errorf("error building kube-apiserver flags: %v", err)
@ -228,7 +250,6 @@ func (b *KubeAPIServerBuilder) buildPod() (*v1.Pod, error) {
for _, path := range b.SSLHostPaths() { for _, path := range b.SSLHostPaths() {
name := strings.Replace(path, "/", "", -1) name := strings.Replace(path, "/", "", -1)
addHostPathMapping(pod, container, name, path) addHostPathMapping(pod, container, name, path)
} }

View File

@ -18,6 +18,7 @@ package model
import ( import (
"fmt" "fmt"
"path/filepath"
"k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/api/v1"
"k8s.io/kops/nodeup/pkg/distros" "k8s.io/kops/nodeup/pkg/distros"
@ -40,7 +41,7 @@ type KubeletBuilder struct {
var _ fi.ModelBuilder = &KubeletBuilder{} var _ fi.ModelBuilder = &KubeletBuilder{}
// Build is responsible for generating the kubelet config // Build is responsible for building the kubelet configuration
func (b *KubeletBuilder) Build(c *fi.ModelBuilderContext) error { func (b *KubeletBuilder) Build(c *fi.ModelBuilderContext) error {
kubeletConfig, err := b.buildKubeletConfig() kubeletConfig, err := b.buildKubeletConfig()
if err != nil { if err != nil {
@ -270,6 +271,12 @@ func (b *KubeletBuilder) buildKubeletConfigSpec() (*kops.KubeletConfigSpec, erro
utils.JsonMergeStruct(c, b.Cluster.Spec.Kubelet) utils.JsonMergeStruct(c, b.Cluster.Spec.Kubelet)
} }
// @check if we are using secure kubelet <-> api settings
if b.UseSecureKubelet() {
// @TODO these filenames need to be a constant somewhere
c.ClientCAFile = filepath.Join(b.PathSrvKubernetes(), "ca.crt")
}
if b.InstanceGroup.Spec.Kubelet != nil { if b.InstanceGroup.Spec.Kubelet != nil {
utils.JsonMergeStruct(c, b.InstanceGroup.Spec.Kubelet) utils.JsonMergeStruct(c, b.InstanceGroup.Spec.Kubelet)
} }

View File

@ -32,18 +32,29 @@ type PKIModelBuilder struct {
var _ fi.ModelBuilder = &PKIModelBuilder{} var _ fi.ModelBuilder = &PKIModelBuilder{}
// Build is responsible for generating the pki assets for the cluster // Build is responsible for generating the various pki assets
func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error { func (b *PKIModelBuilder) Build(c *fi.ModelBuilderContext) error {
{ {
t := &fitasks.Keypair{ t := &fitasks.Keypair{
Name: fi.String("kubelet"), Name: fi.String("kubelet"),
Lifecycle: b.Lifecycle, Lifecycle: b.Lifecycle,
Subject: "o=" + user.NodesGroup + ",cn=kubelet",
Type: "client", Subject: "o=" + user.NodesGroup + ",cn=kubelet",
Type: "client",
} }
c.AddTask(t) c.AddTask(t)
} }
{
// Generate a kubelet client certificate for api to speak securely to kubelets. This change was first
// introduced in https://github.com/kubernetes/kops/pull/2831 where server.cert/key were used. With kubernetes >= 1.7
// the certificate usage is being checked (obviously the above was server not client certificate) and so now fails
c.AddTask(&fitasks.Keypair{
Name: fi.String("kubelet-api"),
Lifecycle: b.Lifecycle,
Subject: "cn=kubelet-api",
Type: "client",
})
}
{ {
t := &fitasks.Keypair{ t := &fitasks.Keypair{
Name: fi.String("kube-scheduler"), Name: fi.String("kube-scheduler"),