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.
Currently permission scope is quite broad. See [iam_roles.md](iam_roles.md) for details and ways to mitigate that.
## Kubernetes API
(this section is a work in progress)
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
The API bearer token is a secret named 'admin'.

View File

@ -198,3 +198,30 @@ func (c *NodeupModelContext) UsesCNI() bool {
}
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
import (
"fmt"
"path/filepath"
"strconv"
"github.com/golang/glog"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/kops/pkg/apis/kops"
"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
@ -35,6 +39,11 @@ func i64(v int64) *int64 {
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 {
if proxies == nil {
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
func b(v bool) *bool {
return fi.Bool(v)
// buildCertificateRequest retrieves the certificate from a keystore
func buildCertificateRequest(c *fi.ModelBuilderContext, b *NodeupModelContext, name, path string) error {
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{}
// 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 {
if !b.IsMaster {
return nil
@ -71,6 +71,19 @@ func (b *KubeAPIServerBuilder) Build(c *fi.ModelBuilderContext) error {
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
{
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)
}
// buildPod is responsible for generating the kube-apiserver pod and thus manifest file
func (b *KubeAPIServerBuilder) buildPod() (*v1.Pod, error) {
kubeAPIServer := b.Cluster.Spec.KubeAPIServer
kubeAPIServer.ClientCAFile = filepath.Join(b.PathSrvKubernetes(), "ca.crt")
@ -150,7 +164,15 @@ func (b *KubeAPIServerBuilder) buildPod() (*v1.Pod, error) {
kubeAPIServer.EtcdServers = []string{"https://127.0.0.1:4001"}
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)
if err != nil {
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() {
name := strings.Replace(path, "/", "", -1)
addHostPathMapping(pod, container, name, path)
}

View File

@ -18,6 +18,7 @@ package model
import (
"fmt"
"path/filepath"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/kops/nodeup/pkg/distros"
@ -40,7 +41,7 @@ type KubeletBuilder struct {
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 {
kubeletConfig, err := b.buildKubeletConfig()
if err != nil {
@ -270,6 +271,12 @@ func (b *KubeletBuilder) buildKubeletConfigSpec() (*kops.KubeletConfigSpec, erro
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 {
utils.JsonMergeStruct(c, b.InstanceGroup.Spec.Kubelet)
}

View File

@ -32,18 +32,29 @@ type PKIModelBuilder struct {
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 {
{
t := &fitasks.Keypair{
Name: fi.String("kubelet"),
Lifecycle: b.Lifecycle,
Subject: "o=" + user.NodesGroup + ",cn=kubelet",
Type: "client",
Subject: "o=" + user.NodesGroup + ",cn=kubelet",
Type: "client",
}
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{
Name: fi.String("kube-scheduler"),