[ macOS/ARM64 | Linux/AMD64 ]
Previous: Launching the VM Cluster
At this point in the guide, we have all the virtual hardware prepared, and we're eager to start installing Kubernetes on it.
However, in order to properly understand all the steps and various magic options of Kubernetes components, it would be worth to stop and look at the Kubernetes architecture from a bird's eye view. Kubernetes is a fairly complex system, made of multiple interconnected components. In a system like that, security is paramount, and must be understood and set up with diligence.
In this chapter, we're going to outline the entire Kubernetes architecture, i.e. list all its components and communication channels. Then we'll explain how each communication channel is secured. Finally, we will prepare a set of certificates and configuration files that we'll use during actual installation of Kubernetes components, in subsequent chapters.
Table of Contents generated with DocToc
- Prerequisites
- Overview of Kubernetes building blocks
- Bootstrapping the security
- Distributing certificates and keys
- Setting up local
kubeconfig
- Summary
Make sure you have completed the previous chapters and have all the necessary packages installed.
Kubernetes is made of multiple components, running as separate processes that communicate with each other. They are split between control plane and worker nodes.
The control plane components include:
etcd
- the central, distributed, highly reliable database holding the entire cluster statekube-apiserver
- the Kubernetes API server, i.e. the public interface of the clusterkube-scheduler
- the component responsible for assigning pods to worker nodeskube-controller-manager
- the component running Kubernetes controllerscloud-controller-manager
- provides integrations specific to cloud provider (AWS, GCP, etc.) - not used in this guide
Worker nodes run the following components:
kubelet
- manages the lifecycle of pods on a given worker nodekube-proxy
- serves as a local proxy/load balancer for Kubernetes services
For technical reasons explained later,
we'll run kubelet
and kube-proxy
on control plane nodes, too.
Now, let's outline all the ways these components communicate with each other.
- every
etcd
instance talks to all otheretcd
instances (peers) kube-apiserver
talks toetcd
as a clientkube-scheduler
talks tokube-apiserver
as a clientkube-controller-manager
talks tokube-apiserver
as a clientkubelet
talks tokube-apiserver
as a clientkube-proxy
talks tokube-apiserver
as a client- also, the
kube-apiserver
talks tokubelet
as a client - for some specific purposes like fetching logs or setting up port forwarding to pods - external clients talk to
kube-apiserver
, typically usingkubectl
- pods running in the cluster may talk to
kube-apiserver
as clients kube-apiserver
may occasionally communicate with services running in the cluster (e.g. admission webhooks)
Most of this communication will be secured using TLS with X.509 certificates. Authentication must be mutual, i.e. both the server and the client must present a valid certificate. Of course, every certificate must be signed by a certificate authority that is trusted by the receiving party.
Even though we are going to use mostly certificates to authenticate to the Kubernetes API server, several other strategies are possible.
Let's quickly discuss the basic authentication model of Kubernetes:
- A human client of a Kubernetes API typically identifies itself as a user,
optionally belonging to one or more groups. Users and groups are not managed by Kubernetes in any way, i.e. there
is no catalogue of users and groups maintained by the cluster. Instead, users and groups are treated like opaque
identifiers, and the API server trusts its selected authentication strategy to determine them.
For example, in case of certificates, when the certificate is valid according to preconfigured CA, the Common Name field is assumed to contain the username, while Organization fields are interpreted as group names. - A non-human client of a Kubernetes API (e.g. a pod running in the cluster) typically authenticates itself using a service account. Unlike users and groups, service accounts are managed by Kubernetes, i.e. they can be created, deleted, etc. When identifying as a service account, an API client uses a JWT token, previously generated and provisioned by the cluster to the pod (see projected volumes). However, the token itself must be signed - unsurprisingly - with a certificate, and this certificate must be preconfigured.
This gives us an overview of all the certificates that we need to prepare for a fully functioning Kubernetes cluster:
etcd
peer certificate, for everyetcd
instance- server certificates:
etcd
server certificate, for everyetcd
instancekube-apiserver
server certificatekubelet
server certificate
- client certificates
kube-apiserver
client certificate to communicate withetcd
kube-apiserver
client certificate to communicate withkubelet
skubelet
client certificate to communicate withkube-apiserver
, for every control and worker nodekube-scheduler
client certificate to communicate withkube-apiserver
kube-controller-manager
client certificate to communicate withkube-apiserver
kube-proxy
client certificate to communicate withkube-apiserver
- client certificates for human users to communicate with the Kubernetes API (
kube-apiserver
)
- certificate and key for verifying and signing service account tokens
Of course, every certificate must be signed by a Certificate Authority. Technically, it is possible to have distinct CAs for different kinds of certificates:
- a CA to sign
etcd
peer certificates - a CA to sign
etcd
server certificate(s) - a CA to sign
kube-apiserer
server certificate - a CA to sign
kubelet
server certificate - a CA to sign
etcd
client certificates - a CA to sign
kube-apiserver
client certificates - a CA to sign
kubelet
client certificates
The previous section presents an exhaustive list of certificates and CAs that could be configured separately. In practice, however, there is no reason to go that far, at least for the purposes of this guide. We'll simplify things in the following ways:
- We'll use a single root CA to sign all the certificates
kube-apiserver
, even though deployed as three separate instances, is seen by its clients as a single service. We are planning to set up a load balancer for it (thegateway
VM) and make it reachable using a single virtual IP address and domain name. For this reason it is natural (and necessary) to have a single server certificate for the Kubernetes API. This certificate will contain SAN entries for all the possible IPs and domain names that can be used to reach the API, including addresses of individual instances, the virtual, load balanced address, as well as Kubernetes-internal IPs and domains.etcd
runs on the same nodes askube-apiserver
, so it is natural to reuse the Kubernetes API certificate foretcd
, to serve both as a peer and server certificate, on everyetcd
instance.- We will also use the main Kubernetes API certificate as the client certificate used to communicate with
etcd
andkubelet
. - Each
kubelet
's server certificate will also serve as its client certificate to communicate withkube-apiserver
.
As for the client certificates used to communicate with kube-apiserver
, we must keep them separate. This is because
we must maintain separate identities for kube-scheduler
, kube-controller-manager
, every node's
kubelet
, kube-proxy
, and external, human users, in order for each of these actors to get the appropriate
set of permissions within the Kubernetes API server.
In this guide, the only human user will be the admin
user, with full permissions to the entire Kubernetes API.
Ultimately, this gives us the following list of certificates to prepare:
- The root CA
- The main Kubernetes API certificate
- The
admin
user certificate - Node (
kubelet
) certificates, separate for each control and worker node - The
kube-scheduler
certificate - The
kube-controller-manager
certificate - The
kube-proxy
certificate - The certificate for signing service account tokens
Every Kubernetes API client needs three pieces of data to communicate with the server: the client certificate, its associated private key, and the CA to verify the server certificate. These three files are usually not configured directly, but rather included into a kubeconfig.
For the purposes of this guide, we'll treat kubeconfigs as simple wrappers over these three files. In their generality, however, they can be more complex. For example, they can include authentication data for multiple users of multiple independent Kubernetes clusters.
We will need to generate a kubeconfig for every client of the Kubernetes API:
- The
admin
user - Each node (i.e.
kubelet
) kube-scheduler
kube-controller-manager
kube-proxy
It's time to generate all the listed certificates and kubeconfigs.
Note
This section is largely based on Kubernetes the Hard Way
There are many tools to generate X.509 certificates. Our utility of choice is cfssl.
Let's create a directory for everything security-related:
mkdir auth && cd auth
The first thing to generate is the root certificate authority. We can do that by preparing a JSON file
representing a Certificate Signing Request and pass it to cfssl
.
Create the ca-csr.json
file:
{
"CN": "Kubernetes",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "PL",
"L": "Krakow",
"O": "Kubernetes",
"OU": "kubenet",
"ST": "Lesser Poland"
}
]
}
All the names in this CSR are arbitrary, you can choose whatever you like.
Generate the CA with:
cfssl gencert -initca ca-csr.json | cfssljson -bare ca
This generates ca.csr
, ca.pem
(the certificate) and ca-key.pem
(the private key).
The .csr
file will not be used and may be discarded.
Note
cfssl
is designed to work like a REST API and returns its result wrapped into JSON.
We use cfssljson
utility to convert it into PEM files.
In order to facilitate signing client & server certificates, we can factor out common settings
into a shared configuration file, the ca-config.json
:
{
"signing": {
"default": {
"expiry": "87600h"
},
"profiles": {
"kubernetes": {
"usages": ["signing", "key encipherment", "server auth", "client auth"],
"expiry": "87600h"
}
}
}
}
Here are some details to note about it:
default
specifies global options, whileprofiles
contains a set of arbitrarily named "profiles" that may override these options. When generating a certificate, the desired profile is selected with a command line option. This way the config file may serve as an aggregate for multiple, independent sets of options.expiry
specifies the validity of a certificate - in this case we set it to 10 years (unfortunately, hour is the largest time unit possible to use here)usages
corresponds to the Key Usage Extension of the X.509 certificate format
Create a kubernetes-csr.json
file:
{
"CN": "kubernetes",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "PL",
"L": "Krakow",
"O": "Kubernetes",
"OU": "kubenet",
"ST": "Lesser Poland"
}
],
"hosts": [
"kubernetes",
"kubernetes.default",
"kubernetes.default.svc",
"kubernetes.default.svc.cluster",
"kubernetes.default.svc.cluster.local",
"10.32.0.1",
"kubernetes.kubenet",
"192.168.1.21",
"control0",
"control0.kubenet",
"192.168.1.11",
"control1",
"control1.kubenet",
"192.168.1.12",
"control2",
"control2.kubenet",
"192.168.1.13",
"127.0.0.1"
]
}
CN and names
for this certificate are arbitrary. What's important is the hosts
list, which includes all the domain
names and IPs that may be used to reach the Kubernetes API, both from outside and inside the Kubernetes cluster:
kubernetes.default.*
are domain names used to communicate with the Kubernetes API from within the cluster, they will resolve to the Kubernetes API internal Service IP- 10.32.0.1 is the Kubernetes API internal Service IP - it may be chosen arbitrarily as long as it is consistent
with configuration of
kube-proxy
and/or other Kubernetes components - we will see that in subsequent chapters kubernetes.kubenet
is the full domain name that resolves to the load-balanced virtual IP of the Kubernetes API from outside the cluster- 192.168.1.21 is the Kubernetes API virtual IP, which we will take care of in another chapter
- the simple name
kubernetes
is resolvable both from outside and inside the cluster controlX
,controlX.kubenet
are control node domain names- 192.168.1.1X are control node IPs
- finally, a 127.0.0.1 entry to allow reaching Kubernetes API via localhost on control nodes
Generate and sign the certificate with:
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=kubernetes \
kubernetes-csr.json | cfssljson -bare kubernetes
The resulting interesting files are kubernetes.pem
and kubernetes-key.pem
.
Create an admin-csr.json
file:
{
"CN": "admin",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "PL",
"L": "Krakow",
"O": "system:masters",
"OU": "kubenet",
"ST": "Lesser Poland"
}
]
}
Then generate a signed certificate and key using:
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=kubernetes \
admin-csr.json | cfssljson -bare admin
This will generate admin.pem
and admin-key.pem
.
Important
The admin
user gets unrestricted access to the Kubernetes API thanks to its magic system:masters
group name.
This is a special group within the Kubernetes RBAC authorization mode
that is bootstrapped to have unlimited permissions.
The CN admin
, interpreted as user name, also has a special meaning.
We need six separate certificates for control and worker nodes. They differ only in names, IPs and hostnames, so let's use
some scripting. Write out the controlX-csr.json
and workerX-csr.json
files:
vmnames=(control{0,1,2} worker{0,1,2})
vmid=0
for vmname in ${vmnames[@]}; do
cat <<EOF > "$vmname-csr.json"
{
"CN": "system:node:$vmname",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "PL",
"L": "Krakow",
"O": "system:nodes",
"OU": "kubenet",
"ST": "Lesser Poland"
}
],
"hosts": [
"$vmname",
"$vmname.kubenet",
"192.168.1.$((10 + $vmid))"
]
}
EOF
vmid=$(($vmid + 1))
done
and generate the certificates:
for vmname in ${vmnames[@]}; do cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=kubernetes \
$vmname-csr.json | cfssljson -bare $vmname
done
Important
system:node:<nodename>
and system:nodes
are magic user and group names interpreted by
Kubernetes node authorization mode.
Create kube-scheduler-csr.json
:
{
"CN": "system:kube-scheduler",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "PL",
"L": "Krakow",
"O": "system:kube-scheduler",
"OU": "kubenet",
"ST": "Lesser Poland"
}
]
}
Generate certificate with:
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=kubernetes \
kube-scheduler-csr.json | cfssljson -bare kube-scheduler
Important
system:kube-scheduler
is a magic string recognized by Kubernetes
RBAC authorization mode
Create kube-controller-manager-csr.json
:
{
"CN": "system:kube-controller-manager",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "PL",
"L": "Krakow",
"O": "system:kube-controller-manager",
"OU": "kubenet",
"ST": "Lesser Poland"
}
]
}
Generate the certificate with:
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=kubernetes \
kube-controller-manager-csr.json | cfssljson -bare kube-controller-manager
Important
system:kube-controller-manager
is a magic string recognized by Kubernetes
RBAC authorization mode
Create kube-proxy-csr.json
:
{
"CN": "system:kube-proxy",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "PL",
"L": "Krakow",
"O": "system:node-proxier",
"OU": "kubenet",
"ST": "Lesser Poland"
}
]
}
Generate the certificate with:
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=kubernetes \
kube-proxy-csr.json | cfssljson -bare kube-proxy
Important
system:kube-proxy
and system:node-proxier
are magic strings recognized by Kubernetes
RBAC authorization mode
Create service-account-csr.json
:
{
"CN": "service-accounts",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "PL",
"L": "Krakow",
"O": "Kubernetes",
"OU": "kubenet",
"ST": "Lesser Poland"
}
]
}
Generate the certificate with:
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=kubernetes \
service-account-csr.json | cfssljson -bare service-account
Let's remove some boilerplate and have a script to turn all the *-csr.json
files into PEM files at once.
Let's save it as genauth.sh
(in the auth
directory).
#!/usr/bin/env bash
set -xe
dir=$(dirname "$0")
gencert() {
name=$1
cfssl gencert \
-ca="$dir/ca.pem" \
-ca-key="$dir/ca-key.pem" \
-config="$dir/ca-config.json" \
-profile=kubernetes \
"$dir/$name-csr.json" | cfssljson -bare $name
}
cfssl gencert -initca "$dir/ca-csr.json" | cfssljson -bare ca
for name in kubernetes admin kube-scheduler kube-controller-manager kube-proxy service-account; do
gencert $name
done
for i in $(seq 0 2); do
gencert control$i
gencert worker$i
done
As already explained, we need a kubeconfig for every Kubernetes API client certificate.
We can make them with the kubectl
command. Below is a script fragment that does this.
You can add it to genauth.sh
.
genkubeconfig() {
cert=$1
user=$2
kubeconfig="$dir/${cert}.kubeconfig"
kubectl config set-cluster kubenet \
--certificate-authority="$dir/ca.pem" \
--embed-certs=true \
--server=https://kubernetes:6443 \
--kubeconfig="$kubeconfig"
kubectl config set-credentials "$user" \
--client-certificate="$dir/${cert}.pem" \
--client-key="$dir/${cert}-key.pem" \
--embed-certs=true \
--kubeconfig="$kubeconfig"
kubectl config set-context default \
--cluster=kubenet \
--user="$user" \
--kubeconfig="$kubeconfig"
kubectl config use-context default \
--kubeconfig="$kubeconfig"
}
genkubeconfig admin admin
genkubeconfig kube-scheduler system:kube-scheduler
genkubeconfig kube-controller-manager system:kube-controller-manager
genkubeconfig kube-proxy system:kube-proxy
for i in $(seq 0 2); do
genkubeconfig control$i system:node:control$i
genkubeconfig worker$i system:node:worker$i
done
The final security-related piece of data, although unrelated to authentication, is a symmetric encryption
key which can be used by kube-apiserver
to encrypt sensitive data stored in etcd
. The key is random, and the only
thing we need to do is to wrap it into a simple YAML file.
Let's do it with a script, genenckey.sh
:
#!/usr/bin/env bash
set -xe
dir=$(dirname "$0")
key=$(head -c 32 /dev/urandom | base64)
cat > "$dir/encryption-config.yaml" <<EOF
kind: EncryptionConfig
apiVersion: v1
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: $key
- identity: {}
EOF
Let's upload all the prepared files into the VMs.
Make sure the VMs are running, as described in the previous chapter.
Also, make sure that vmsshsetup.sh
has been run for all the VMs.
Then, upload the files with a script, deployauth.sh
:
#!/usr/bin/env bash
set -xe
dir=$(dirname "$0")
for i in $(seq 0 2); do
vmname=control$i
scp \
"$dir/ca.pem" \
"$dir/ca-key.pem" \
"$dir/kubernetes-key.pem" \
"$dir/kubernetes.pem" \
"$dir/service-account-key.pem" \
"$dir/service-account.pem" \
"$dir/admin.kubeconfig" \
"$dir/kube-controller-manager.kubeconfig" \
"$dir/kube-scheduler.kubeconfig" \
"$dir/encryption-config.yaml" \
"$dir/$vmname.pem" \
"$dir/$vmname-key.pem" \
"$dir/$vmname.kubeconfig" \
"$dir/kube-proxy.kubeconfig" \
ubuntu@$vmname:~
done
for i in $(seq 0 2); do
vmname=worker$i
scp \
"$dir/ca.pem" \
"$dir/$vmname.pem" \
"$dir/$vmname-key.pem" \
"$dir/$vmname.kubeconfig" \
"$dir/kube-proxy.kubeconfig" \
ubuntu@$vmname:~
done
scp "$dir/ca.pem" ubuntu@gateway:~
As for the admin
certificate, we want to use it locally, so instead of creating a separate kubeconfig
file,
we add it into a local, default one (i.e. ~/.kube/config
), which may already exist and contain entries for other
Kubernetes clusters.
We can do this with the following script, setuplocalkubeconfig.sh
:
#!/usr/bin/env bash
set -xe
dir=$(dirname "$0")
kubectl config set-cluster kubenet \
--certificate-authority="$dir/ca.pem" \
--embed-certs=true \
--server=https://kubernetes:6443
kubectl config set-credentials admin \
--client-certificate="$dir/admin.pem" \
--client-key="$dir/admin-key.pem" \
--embed-certs=true
kubectl config set-context kubenet \
--cluster=kubenet \
--user=admin
kubectl config use-context kubenet
This will allow us to use the kubectl
command locally to communicate with the (currently nonexistent) Kubernetes
cluster.
In this chapter, we have:
- learned about all the components that a Kubernetes deployment is made of
- thoroughly understood which of these components communicate with each other and how this communication is secured
- learned the basic architecture of Kubernetes API client authentication
- generated all the certificates and configuration files necessary for secure Kubernetes deployment
- uploaded the files into the VMs