Here we have a simple Python web application that connects to MySQL database. On success, the application displays a successful message.

app.py

import os
from flask import Flask

app = Flask(__name__)

@app.route("/")
def main ():

mysql.connector.connect (host='mysql', database='mysql',
user='root', password='p@ssw0rd')

return render_template('hello.html', color=fetchcolor())

if __name__ == "__main__"
	app.run(host="0.0.0.0", port="8080")

If you look closely into the code, you will see the host name, username, and password are hard coded, which of course not a good idea.

As we learn in the previous lecture, one option would be to move these values into a ConfigMap. However, ConfigMap stores configuration data in plain text format.

  • So while it would be okay to move the host name and username into a config map, it is definitely not the right place to store a password. This is where secrets come in.

Creating a Secret

Secrets are used to store sensitive information like passwords or keys. They’re similar to ConfigMaps except that they’re stored in an encoded format, i.e. the data is converted into a format that is not human-readable. As with ConfigMaps, there are two steps involved in working with secrets.

  • First, create the secret. Second, inject it into the pod.

There are two ways of creating a secret.

  1. The imperative way, without using a secret definition file.
  • With the imperative method, you can directly specify the key value pairs in the command line itself.
  • To create a secret of the given values, run:
$ kubectl create secret generic \
	<secret-name> --from-literal=<key>=<value>

$ kubectl create secret generic \
	app-secret --from-literal=DB_Host=mysql \
				--from-literal=DB_User=root \
				--from-literal=DB_Password=p@ssw0rd
  • The --from-literal option is used to specify the key value pairs in the command itself.
  • If you wish to add additional key value pairs, simply specify the from literal options multiple times. However, this could get complicated when you have too many secrets to pass in.

Another way to input the secret data is through a file.

  • Use the --from-file option to specify a path to the file that contains the required data. The data from this file is read and stored under the name of the file.

app_secret

DB_HOST: mysql
DB_User: root
DB_Paswword: p@ssw0rd
$ kubectl create secret generic \
	app-secret --from-file=app_secret.properties
  1. The declarative way, by using a secret definition file.
  • For this, we create a definition file, just like how we did for the ConfigMap, and use the same app_secret from before. app_secret
DB_HOST: mysql
DB_User: root
DB_Password: p@ssw0rd

secret-data.yaml

apiVersion: v1
kind: Secret
metadata:
  name: app-secret
data:
  DB_HOST: mysql
  DB_User: root
  DB_Password: p@ssw0rd
  • However, one thing we discussed about secrets was that they’re used to store sensitive data and are stored in an encoded format. Here we have specified the data in plain text which is not very safe.
  • So while creating a secret with a declarative approach, you must specify the secret values in an encoded format.
  • But how do you convert the data from plain text to an encoded format? On a Linux host, run the command echo-n, followed by the text you’re trying to convert which is my SQL in this case. And pipe that to the base 64 utility.
echo -n 'mysql' | base64
# Output: bXlzcWw=

echo -n 'root' | base64
# Output: cm9vdA==

echo -n 'p@ssw0rd' | base64
# Output: cEAzc3cwcmQ=

  • You can use these encoded secrets in the secret definition file and create the secret with: $ kubectl create -f secret-data.yaml

Viewing Secrets

To view secrets, run the $ kubectl get secrets command.

  • This lists the newly created secret along with another secret previously created by Kubernetes for its internal purposes.

To view more information on the newly created secret run the $ kubectl describe secret command.

  • This shows the attributes in the secret but hides the value themselves.

To view the values as well, run the $ kubectl get secret command with the output displayed in a YAML format. Using the -o yaml option. You can now see the hand coded values as well.

Now, how do you decode encoded values? Use the same base 64 command used earlier to encode it but this time add a decode option to it.

echo -n 'bXlzcWw=' | base64 --decode
# Output: mysql

echo -n 'cm9vdA==' | base64 --decode
# Output: root

echo -n 'cEAzc3cwcmQ=' | base64 --decode
# Output: p@ssw0rd


Configuring the Secret with a Pod

Now that we have secret created let us proceed with step two, configuring it with a pod. Here is a simple pod definition file that runs an application.

secret-data.yaml

apiVersion: v1
kind: Secret
metadata:
  name: app-secret # This is what connects the Secret to the pod
data: 
  DB_Host=bXlzcWw= 
  DB_User=cm9vdA==
  DB_Password=cEAzc3cwcmQ=
  

pod-definition.yaml

apiVersion: v1
kind: Pod
metadata:
  name: simple-webapp-color
spec:
  containers:
  - name: simple-webapp-color
    image: simple-webapp-color
    ports:
      - containerPort: 8080
    envFrom:
    - secretRef:
        name: app-secret # This is what connects the Secret to the pod

To inject an environment variable add a new property to the container called envFrom.

  • This property is a list so we can pass as many environment variables as required. Each item in the list corresponds to a secret item.
  • Specify the name of the secret we created earlier. Creating the pod definition file now makes the data in the secret available as environment variables for the application.

What we just saw was injecting secrets as environment variables into the pods. Again, this was the relevant YAML for injecting environment variables:

    envFrom:
    - secretRef:
        name: app-secret # This is what connects the Secret to the pod

There are other ways to inject secrets into pods.

  • You can inject as single environment variables
env:
  - name
    valueFrom:
      secretKeyRef:
        name: app-secret
        key: DB_Password
  • You can inject the whole secret as files in a volume.
volumes:
- name: app-secret-volume
  secret:
    secretName: app-secret
  • If you were to mount the secret as a volume in the pod. Each attribute in the secret is created as a file with the value of the secret as its content.
  • In this case, since we have three attributes in our secret three files are created, and if we look at the contents of the DB_Password file, we see the password in it.

Important Things to Note About Secrets

Note that secrets are not encrypted. They’re only encoded, meaning anyone can look up the file that you created for secrets or get the secret object and then decode it using the methods that we discussed before to see the confidential data.

  • Remember not to check in your secret definition files along with your code when you push to GitHub or something.
  • There are a lot of lots of repositories already on GitHub where users have pushed their secret objects along with their code, the rest of the code. You could easily get those secret objects read them, and then just decode them using the basic 64 option that we just discussed, and you can get to see the what the underlying passwords are.

Another note is that the secrets are not encrypted in ETCD, so none of the data in ETCD is encrypted by default, so consider enabling encryption at rest.

Also note that anyone able to create pods or deployments in the same name space can access the secrets as well.

  • Once you create a secret in a particular name space if anyone with access to creating a pod or deployment in the same name space just goes in and creates a pod or deployment and uses that same secret, then they’re able to see the secret objects mounted onto those pods.
  • Consider configuring role-based access control to restrict access.

Finally, consider third party secret provider, such as AWS provider or Azure provider or GCP provider or the Vault provider. This way, the secrets are stored not in ETCD but in external secret provider, and that those providers take care of most of the security.

How Secrets are Stored in ETCD

We want to ensure that data is encrypted when written to etcd by enabling encryption at rest, i.e. after restarting your kube-apiserver, any newly created or updated Secret (or other resource kinds configured in EncryptionConfiguration) should be encrypted when stored.

This next section uses the etcdctl tool. If you don’t have it, run $ apt-get install etcd-client . Check the version with $ etcdctl version, and just remember that’s the client only, the server is still running on as a pod.

  • You will see that the server is running on a pod if you run $ kubectl get pods -n kube-system.
  • Next, check if you have a certificate file with $ ls /etc/kubernetes/pki/etcd/ and you should see all of the certificate file for authenticating to the ETCD server.

Create a Secret

Let’s take the following secret…

apiVersion: v1
kind: Secret
metadata:
  name: my-secret
type: Opaque
data:
  key1: supersecret

And run the following with etcdctl, remembering to set the version to 3.

ETCDCTL_API=3 etcdctl \ *
-- cacert=/etc/kubernetes/pki/etcd/ca.crt
-- cert=/etc/kubernetes/pki/etcd/server.crt \
-- key=/etc/kubernetes/pki/etcd/server.key
get /registry/secrets/default/my-secret | hexdump -C
  • The output is similar to this. You see, however, the secret “supersecret” is still visible in plain text, therefore it is not encrypted.
00000000  2f 72 65 67 69 73 74 72  79 2f 73 65 63 72 65 74  |/registry/secret| 
00000010  73 2f 64 65 66 61 75 6c  74 2f 6d 79 2d 73 65 63  |s/default/my-sec|
00000020  72 65 74 0a 6b 38 73 00  0a 0c 0a 02 76 31 12 06  |ret.k8s.....v1..|
00000030  53 65 63 72 65 74 12 d0  01 0a b0 01 0a 09 6d 79  |Secret........my| 
00000040  2d 73 65 63 72 65 74 12  00 1a 07 64 65 66 61 75  |-secret....defau| 
00000050  6c 74 22 00 2a 24 64 66  65 39 37 63 36 32 2d 35  |lt".*$dfe97c62-5|
00000060  61 61 31 2d 34 36 61 38  2d 62 37 31 63 2d 66 66  |aa1-46a8-b71c-ff|
00000070  61 30 63 64 34 63 30 38  65 63 32 00 38 00 42 08  |a0cd4c08ec2.8.B.|
00000080  08 d5 c7 d8 9a 06 10 00  8a 01 61 0a 0e 6b 75 62  |..........a..kub|
00000090  65 63 74 6c 2d 63 72 65  61 74 65 12 06 55 70 64  |ectl-create..Upd|
000000a0  61 74 65 1a 02 76 31 22  08 08 d5 c7 d8 9a 06 10  |ate..v1"........|
000000b0  00 32 08 46 69 65 6c 64  73 56 31 3a 2d 0a 2b 7b  |.2.FieldsV1:-.+{|
000000c0  22 66 3a 64 61 74 61 22  3a 7b 22 2e 22 3a 7b 7d  |"f:data":{".":{}| 
000000d0  2c 22 66 3a 6b 65 79 31  22 3a 7b 7d 7d 2c 22 66  |,"f:key1":{}},"f|
000000e0  3a 74 79 70 65 22 3a 7b  7d 7d 42 00 12 13 0a 04  |:type":{}}B.....| 
000000f0  6b 65 79 31 12 0b 73 75  70 65 72 73 65 63 72 65  |key1..supersecre|
00000100  74 1a 06 4f 70 61 71 75  65 1a 00 22 00 0a        |t..Opaque.."..| 
0000010e

First step to mitigating this is to determine if encryption address is already enabled or not.

  • This is done with a property called --encryption-provider-config in the [[KubeAPIServer]]. We can see what options the Kube API Server is running with by listing the processes , grepping for ‘kube-api’ and then ‘encryption-provider-config’: $ ps -aux | grep kube-api | encryption-provider-config

In this example, it does not return a result, therefore it is not configured. This can also be verified in kubeadm setups if you look at this particular file: $ cat /etc/kubernetes/manifests/kube-apiserver.yaml

  • Again in this example, this option is not listed here, which means that encryption at rest is not enabled.

Create a Configuration File

The next step is to create a configuration file and then pass in the --encryption-provider-config option. The following configuration file is an example from the k8s documentation and should not be used at face value in your cluster.

---
#
# CAUTION: this is an example configuration.
#          Do not use this for your own cluster!
#
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  - configmaps
  - pandas.awesome.bears.example # a custom resource API
  providers:
  - identity: {} # plain text, in other words NO encryption
  - aesgcm:
      keys:
      - name: key1
        secret: c2VjcmV0IGlzIHNlY3VyZQ==
      - name: key2
        secret: dGhpcyBpcyBwYXNzd29yZA==
  - aescbc:
      keys:
      - name: key1
        secret: c2VjcmV0IGlzIHNlY3VyZQ==
      - name: key2
        secret: dGhpcyBpcyBwYXNzd29yZA==
  - secretbox:
      keys:
      - name: key1
        secret: YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=
- resources:
  - events
  providers:
  - identity: {} # do not encrypt Events even though *.* is specified below
- resources:
  - '*.apps' # wildcard match requires Kubernetes 1.27 or later
  providers:
  - aescbc:
      keys:
      - name: key2
        secret: c2VjcmV0IGlzIHNlY3VyZSwgb3IgaXMgaXQ/Cg==
- resources:
  - '*.*' # wildcard match requires Kubernetes 1.27 or later
  providers:
  - aescbc:
      keys:
      - name: key3
        secret: c2VjcmV0IGlzIHNlY3VyZSwgSSB0aGluaw==

You can pick and choose which resources you want to encrypt; you may have pods and deployments and secrets and services. Whatever the case, you want to store all of your secrets as encrypted.

  • You might not want to encrypt everything because not everything is confidential, though, so you need not necessarily encrypt and save all the data about pods and deployments.

Under resources, you specify the targets, in our case the targets are:

  • secrets
  • configmaps
  • pandas.awesome.bears.example (a custom resource API)

You can encrypt something using a set of providers, and the default one is called identity. However, the identity provider just means that there’s no encryption at all, so resources are written as is without any encryption.

One thing to note here is the order of the items in the providers list. This order matters because whatever the first one is will be what’s going to be used for encryption.

  • This means that since identity is the first one listed, there will be no encryption.

Create a Simple EncryptionConfiguration

Let’s create a much simpler version of this file using AES CBC as the first one which will be used for encryption. This requires a secret object to be used as an encryption key, so we can generate a 32 by random key using: $ head -c 32 /dev/urandom | base64

Now we can use this random key in the secret field under the keys attribute in the new file:

enc.yaml

---
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
      - pandas.awesome.bears.example
    providers:
      - aescbc:
          keys:
            - name: key1
              # See the following text for more details about the secret value
              secret: y0xTt+U6xgRdNxe4nDYYsij0GgRDoUYC+WAwOKeNfPs= # base 64 encoded secret
      - identity: {} # this fallback allows reading unencrypted secrets;
                     # for example, during initial migration

Now that the encryption configuration has been established, we can reference the configuration in the kube-api-server.yaml file which was established by kubeadm at the location/etc/kubernetes/manifests/kube-apiserver.yaml.

  • Essentially what we’re doing here is establishing the encryption method defined in enc.yaml, and mounting that local definition to the Kube API server’s corresponding directory. This is what the file may look like:

kube-api-server.yaml

---
#
# This is a fragment of a manifest for a static Pod.
# Check whether this is correct for your cluster and for your API server.
#
apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 10.20.30.40:443
  creationTimestamp: null
  labels:
    app.kubernetes.io/component: kube-apiserver
    tier: control-plane
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-apiserver
    ...
    - --encryption-provider-config=/etc/kubernetes/enc/enc.yaml  # add this line
    volumeMounts:
    ...
    - name: enc                           # add this line
      mountPath: /etc/kubernetes/enc      # add this line
      readOnly: true                      # add this line
    ...
  volumes:
  ...
  - name: enc                             # add this line
    hostPath:                             # add this line
      path: /etc/kubernetes/enc           # add this line
      type: DirectoryOrCreate             # add this line
  ...

In the Kube API server, add the file path to the --encryption-provider-config option. Since the file is created locally, it has to be mounted as a volume like we saw before in [[2.10 - Multiple Schedulers]] and [[4.3 - Configuring ConfigMaps]].

Two more things to note are the mountPath and the hostPath. You can see that both are located at /etc/kubernetes/enc

  • hostPath: Specifies a directory on the host machine (path: /etc/kubernetes/enc) that will be mounted into the Pod.
  • mountPath: Specifies the path within the container where the volume will be mounted. Here, it’s /etc/kubernetes/enc.

Having the same path specified for both mountPath and hostPath indicates that you’re essentially mounting a directory from the host machine directly into the container at the same location. This configuration is useful when you want to make host machine files or directories accessible within the container.

  • If you haven’t created this path locally, you must create the path and ensure that the local file is in the directory you’ve specified for the hostPath. This way anything that’s available locally in the hostPath is going to be available in the mountPath.
mkdir /etc/kubernetes/enc
mv enc.yaml /etc/kubernetes/enc/
ls /etc/kubernetes/enc

After making these changes, the Kube API server should restart. You can run $ crictl pods to check on the status of the Kube API Server deployment. Once you confirm it is running you can again run $ ps aux | grep kube-api | grep encryption-provider-config to confirm the Kube API server is now running with the --encryption-provider-config option.

Create Another Secrets File

Let’s create another secrets object. Secrets are now created with encryption was enabled. $ kubectl create secret generic my-secret-2 --from-literal=key2=topsecret

View the secret using $ k get secret

Run this command again, except this time for the secret we just created, my-secret-2.

ETCDCTL_API=3 etcdctl \ *
-- cacert=/etc/kubernetes/pki/etcd/ca.crt
-- cert=/etc/kubernetes/pki/etcd/server.crt \
-- key=/etc/kubernetes/pki/etcd/server.key
get /registry/secrets/default/my-secret-2 | hexdump -C
  • This time when you append the | hexdump -C bit at the end, the output will be encrypted, and therefore the secret that we specified before, “topsecret” will not be able to be seen in plaintext.
  • We can confirm that encryption is working, but if you dump the previous one, you will still see the secret because after encryption is enabled, only things that you create newly will be encrypted.

However, if you update an existing configuration, then that will be re-encrypted. So essentially what you must do is update the old secret objects with the same data. An easy command for this is: $ kubectl get secrets --all-namespaces -o json | kubectl replace -f -

And that’s it for encrypting secrets at REST!