Low level/Cloud agnostic method to give access to your cluster with A client certificate.
This is only needed for local/on-premise clusters. All major cloud provider already have their own recommended access control, which should be much easier to use.
Let's say we have a new user called "John". First, we need to generate a private rsa key and a CSR for the user. The private key can easily be created with this command:
openssl genrsa -out john.key 4096
The CSR is a bit more complicated. We need to make sure that:
- The name of the user is used in the Common Name (CN) field: this will be used to identify him against the API Server.
- The group name is used in the Organisation (O) field: this will be used to identify the group against the API Server.
We will use the following configuration file to generate the CSR:
# /csr.cnf
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
[ dn ]
CN = john
O = users
[ v3_ext ]
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage=keyEncipherment,dataEncipherment
extendedKeyUsage=clientAuth
Using the above configuration file (saved in csr.cnf
), the CSR can be created using the following command:
openssl req -config ./csr.cnf -new -key ./john.key -nodes -out john.csr
Ideally, it would be John's responsibility to do these steps. Then, once the .csr file is created, "John" would send it to the platform administrator/s so he can sign it using the cluster Certificate Authority. That’s what we’ll do in the next step.
The signature of the .csr
file will result in the creation of a certificate. This one
will be used to authenticate each request the user (John) will send to the API Server.
We will start by creating a Kubernetes CertificateSigninRequest resource.
We will use the following specification and save it in csr.yaml
.
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: john-csr
spec:
signerName: kubernetes.io/kube-apiserver-client
groups:
- system:authenticated
request: ${BASE64_CSR}
usages:
- client auth
- digital signature
- key encipherment
As we can see, the value of the request key is the content of the BASE64_CSR environment variable.
The first step is to get the base64 encoding of the .csr
file generated for John and
then use the envsubst
binary to substitute the value of this variable before creating the resource.
# Encoding the .csr file in base64
export BASE64_CSR=$(cat ./john.csr | base64 | tr -d '\n')
# Substitution of the BASE64_CSR env variable and creation of the CertificateSigninRequest resource
cat csr.yaml | envsubst | kubectl apply -f -
# Checking the status of the newly created CSR
$ kubectl get csr
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
john-csr 5s kubernetes.io/kube-apiserver-client kubernetes-admin <none> Pending
We can then approve this CSR with this command:
kubectl certificate approve john-csr
Checking the status of the CSR once again, we can see it’s now approved and issued.
$ kubectl get csr
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
john-csr 41s kubernetes.io/kube-apiserver-client kubernetes-admin <none> Approved,Issued
You can extract the certificate from the CSR resource and save it in a file to check what’s inside. E.g.
# download and decode john's certificate
kubectl get csr john-csr -o jsonpath='{.status.certificate}' | base64 --decode > ./downloaded-john.crt
# show the certificate
openssl x509 -in ./downloaded-john.crt -noout -text
We now have to send Dave the information he needs to configure his local kubectl client to communicate with our cluster.
We’ll first create a kubeconfig.template
file, with the following content, that we’ll use as a template.
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: ${CLUSTER_CA}
server: ${CLUSTER_ENDPOINT}
name: ${CLUSTER_NAME}
users:
- name: ${USER}
user:
client-certificate-data: ${CLIENT_CERTIFICATE_DATA}
contexts:
- context:
cluster: ${CLUSTER_NAME}
user: dave
name: ${USER}-${CLUSTER_NAME}
current-context: ${USER}-${CLUSTER_NAME}
To build a base kube config from this template, we first need to set all the needed environment variables:
# User identifier
export USER="john"
# Cluster Name (get it from the current context)
export CLUSTER_NAME=$(kubectl config view --minify -o jsonpath={.current-context})
# Client certificate
export CLIENT_CERTIFICATE_DATA=$(kubectl get csr john-csr -o jsonpath='{.status.certificate}')
# Cluster Certificate Authority
export CLUSTER_CA=$(kubectl config view --raw -o json | jq -r '.clusters[] | select(.name == "'$(kubectl config current-context)'") | .cluster."certificate-authority-data"')
# API Server endpoint
export CLUSTER_ENDPOINT=$(kubectl config view --raw -o json | jq -r '.clusters[] | select(.name == "'$(kubectl config current-context)'") | .cluster."server"')
echo "user: $USER, cluster name: $CLUSTER_NAME, client certificate (length): ${#CLIENT_CERTIFICATE_DATA}, Cluster Certificate Authority (length): ${#CLUSTER_CA}, API Server endpoint: $CLUSTER_ENDPOINT"
Then, we can substitute them using the convenient envsubst
utility:
cat ./kubeconfig.template | envsubst > ./john-kubeconfig
We can now send this kubeconfig file to "John" who will just need to add his private key inside of it and he will be fine to communicate with the cluster.
NOTE:: For the users to be able to access the cluster from other computer in the local network, make sure that the IP of
CLUSTER_ENDPOINT
is accessible on the local network. If the IP is127.0.0.1
it won't be visible to other computer.
In order to use the kubeconfig, John can set the KUBECONFIG
environment variable with the path towards the file.
export KUBECONFIG=$PWD/john-kubeconfig
Note: There are different ways to use a Kubernetes configuration: setting the KUBECONFIG
environment variable, adding a new entry in the default $HOME/.kube/config
file, or using the --kubeconfig
flag on each kubectl command.
To add his private key, John.key generated at the beginning of the process, "John" can use this command:
$ kubectl config set-credentials john --client-key=$PWD/john.key --embed-certs=true
The --embed-certs
flag is needed to generate a standalone kubeconfig, that will work as-is on another host.
It will create the key client-key-data within the user entry of the kubeconfig file and set the base64 encoding of john.key
as the value.
If everything is fine, John should be able to check the version of the server (and the client) with the following command:
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"25", GitVersion:"v1.25.2", GitCommit:"5835544ca568b757a8ecae5c153f317e5736700e", GitTreeState:"clean", BuildDate:"2022-09-21T14:33:49Z", GoVersion:"go1.19.1", Compiler:"gc", Platform:"linux/amd64"}
Kustomize Version: v4.5.7
Server Version: version.Info{Major:"1", Minor:"24", GitVersion:"v1.24.0", GitCommit:"4ce5a8954017644c5420bae81d72b09b735c21f0", GitTreeState:"clean", BuildDate:"2022-05-25T22:55:08Z", GoVersion:"go1.18.1", Compiler:"gc", Platform:"linux/amd64"}
To make it easier for the admins, you can use the
create_user.sh script to add a new user
and create the user's kubeconfig
in one command.