Kubernetes External-Secrets and Vault, a powerful combination for secure secret management.

03.07.2023Stefan Welsch
Cloud Kubernetes Security b-nova techup Stay Tuned

banner

In modern application development, secrets such as access data, API keys and certificates are indispensable. Since we try out a lot of things at b-nova, a lot of secrets have been distributed in different places over the last few years. For example, we have secrets in the AWS Secrets Manager, which we read out via API in various internal applications. We have Secrets stored in Github, which we need to run our pipelines or deploy to the different environments. Furthermore, we have several Kubernetes clusters in which various secrets are also stored. But now we have reached the point where we slowly but surely need to bring some order back into the management of our secrets. The reasons for this are obvious.

  • Secrets are currently stored in several places and there is no SSOT (Single Source of Truth).
  • Secrets are stored redundantly. This means that if you adjust a secret, you have to adjust it in several places.
  • The overview of which secret is where is no longer guaranteed.
  • It is very difficult to manage who has access to which secret.
  • Life-cycle management: Automation and management of the secrets rotation can become complex.
  • Some of the secrets are unencrypted (K8S Secrets)

So the question arose as to how we could get the secret handling back under control. We quickly decided on Hashicorp Vault, as this tool fulfils all our requirements for a secret manager with flying colours.

  • Encryption: Vault stores secrets in encrypted form and offers additional security.
  • Access control: Vault enables fine-grained access control through ACLs and roles.
  • Lifecycle management: Vault supports automation of secret rotation and management.

The Vault server was set up very quickly and the secrets were transferred quickly. But how do we access the secrets from the individual applications, from GitHub and from the K8S cluster? There are several possibilities that we have considered. Most of our applications are currently running on K8s. The secrets are injected into the applications via secret reference to the K8s secrets as environment variables during deployment.

A variant would be, for example, to read out the secrets from Vault via API. The advantage of this would be that the secret is only read where it is really needed. No one would be able to read any passwords via the secrets. The disadvantage, however, is that we have to program the API access into every application first. Another way to manage secrets securely and efficiently in Kubernetes is to use Kubernetes External-Secrets in combination with HashiCorp Vault as external secret storage. So today we’re going to look at how these two technologies work together and how they can optimise the handling of Secrets in Kubernetes clusters.

First, let’s take a look at what the Kubernetes External-Secrets Operator even is.

Kubernetes External-Secrets Operator

The Kubernetes External-Secrets Operator is a Kubernetes controller that enables the synchronisation of secrets from external sources, such as HashiCorp Vault, AWS Secrets Manager or Google Cloud Secret Manager, into Kubernetes clusters. It monitors the cluster for the presence of custom resource definitions (CRDs) called ExternalSecrets. These CRDs contain metadata describing how the Secrets should be retrieved from the external source and stored in Kubernetes Secrets. The operator reads the configuration, retrieves the appropriate Secrets from the external source and creates Kubernetes Secrets containing these Secrets. By using the External-Secrets Operator, developers and administrators can centrally manage secrets in external secrets stores, while applications in Kubernetes continue to use the native Kubernetes secrets. This increases security and enables central management of Secrets, access controls and lifecycle management.

The External-Secrets Operator can be installed via Helm or kubectl. Here is an example of how to install it using Helm:

1
2
3
helm repo add external-secrets https://godaddy.github.io/kubernetes-external-secrets/
helm repo update
helm install external-secrets external-secrets/kubernetes-external-secrets

Create Secret Store

Once the operator is installed, the first thing we need to do is create a SecretStore. To do this, we create a file called secret-store.yml with the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: external-secrets.io/v1beta1
kind: SecretStore # You could also choose ClusterSecretStore 
metadata:
  name: secret-store-vault
  namespace: external-secret-test
spec:
  provider:
    vault:
      server: "https://your-vault-server:8200"
      # Version is the Vault KV secret engine version.
      # This can be either "v1" or "v2", defaults to "v2".
      version: "v2"
      auth:
        # points to a secret that contains a vault token
        # https://www.vaultproject.io/docs/auth/token
        tokenSecretRef:
          name: "vault-token"
          key: "token

We could also create a global ClusterSecretStore here. This would not have to be defined per namespace, but would be available across all namespaces.

We now define the address of our vault server and the version of the KV (Key Value) Secret Engine. Version 2 of the KV Secret Engine now also supports the versioning of secrets. Now we create a namespace and try to create the SecretStore in it:

1
2
3
4
kubectl create namespace external-secret-test
kubectl apply -f secret-store.yml

secretstore.external-secrets.io/secret-store-vault created

As we can see, the store was successfully created. Let’s see if it works properly:

1
2
3
4
k get secretstore -n external-secret-test

NAME AGE STATUS CAPABILITIES READY
secret-store-vault 45s InvalidProviderConfig False

As we can see, the status is InvalidProviderConfig, so something seems to be wrong. Let’s find out what exactly is going wrong:

1
2
3
4
5
6
7
8
9
kubectl describe secretstor secret-store-vault -n external-secret-test

Name: secret-store-vault
namespace: external-secret-test
....
Events:
Type Reason Age From Message
  ---- ------ ---- ---- -------
Warning InvalidProviderConfig 7m29s (x19 over 29m) secret-store cannot get Kubernetes secret "vault-token": secrets "vault-token" not found

We see that the token we specified in our configuration does not exist. So now let’s generate a token.

Generate a token

In this techup I assume that the Vault Server is already fully set up. If this is not yet the case, you would have to set it up first. Detailed instructions can be found directly on the Hashicorp Vault page. For our application we now need a corresponding Vault user. For example, we could add a new userpass method under Access -> AuthMethods. As the name suggests, you can use it to create a user who can then authenticate himself with a password.

image-20230322063519808

In the userpass auth method we can then create a new user.

image-20230322072435202

Once the new user is created, we need to log in to Vault with it to get the required token. We select Username as the method in the login mask and enter the previously given access data. Afterwards we can copy the token of the user via the user menu.

image-20230322072617638

image-20230322072637073

Now we have to store the secret in our namespace. The quickest way to do this is to use kubectl:

1
2
kubectl create secret -n external-secret-test generic vault-token \
    --from-literal=token='your-copied-token'

Now let’s see if the secret store works correctly after creating the secret:

1
2
3
4
kubectl get secretstores -n external-secret-test

NAME AGE STATUS CAPABILITIES READY
secret-store-vault 2m Valid ReadWrite True

We now see that the status is “Valid” and the Secret Store is working.

Once the Secret Store has been successfully set up, we can manage the external secrets. The External-Secrets Operator also offers us a CRD for this. Before we set up the external secret, however, we first want to create it in the vault. Under kv2 we create a secret with the path ‘application/secrets’. Here we can now create a key/value pair with the content MY_TOKEN=1234567890.

image-20230428140842237

Once we have created the secret, we can create another file called external-secret.yml with the following content. Then we create the secret with kubectl apply -f external-secret.yml.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: external-secret-github
  namespace: external-secret-test
spec:
  refreshInterval: "15s" # The secrets will be refreshed every 15 seconds
  secretStoreRef:
    name: secret-store-vault # Reference to the secret store we have created above
    kind: SecretStore 
  target:
    name: my-secret # K8S secret name
  data:
    - secretKey: MY_TOKEN # K8S secret key in the secret
      remoteRef:
        key: kv2/application/secrets # Path to the vault secret
        property: MY_TOKEN # Property name of the secret 

Let’s check again if everything is working properly:

1
2
3
4
kubectl get externalsecret -n external-secret-test

NAME STORE REFRESH INTERVAL STATUS READY
external-secret-github secret-store-vault 15s SecretSynced True

To get more information, for example in case of an error, we can use this command:

1
kubectl describe externalsecret external-secret-github -n external-secret-test

The secret should now have been synchronised and we can now look at it:

1
2
3
4
5
6
kubectl get secret -n external-secret-test ⎈ do-fra1-dev-cluster
NAME TYPE DATA AGE
b-nova-dev kubernetes.io/dockerconfigjson 1 37d
default-token-qrltt kubernetes.io/service-account-token 3 37d
my-secret Opaque 1 15s
vault-token Opaque 1 37d

As we can see, the secret named my-secret has been successfully created. If we look at the content, we can also see that the content is the same as the one stored in the vault.

1
2
3
4
5
kubectl get secret my-secret -n external-secret-test -o jsonpath='{.data}'
{"MY_TOKEN":"MTIzNDU2Nzg5MA=="}

echo "MTIzNDU2Nzg5MA==" | base64 -d
1234567890

Now the expected value is in the Secret. Now let’s test whether the update of a secret works. To do this, we create a new version of the secret in the vault and change the value:

image-20230428141709340

We now have to wait a maximum of 15 seconds and should then also see the new secret in Kubernetes.

1
2
3
4
5
kubectl get secret my-secret -n external-secret-test -o jsonpath='{.data}' 
{"MY_TOKEN":"MDk4NzY1NDMyMQo="}

echo "MDk4NzY1NDMyMQo=" | base64 -d
0987654321

Perfect, we now have the new secret available in our new namespace and can use it directly in our application as an environment variable or volume via a secret reference. Detailed instructions can be found directly on the Kubernetes page.

Summary

Today we looked at how to centrally manage your secrets and easily synchronise them into a namespace using the Kubernetes Secrets Operator. No adjustments to the code in the application are necessary for this. This is a very elegant way, as the secrets are only delivered when they are really needed by the application. Stay tuned! 🚀

This TechUp was translated by our automatic Markdown Translator. 🙌

Stefan Welsch

Stefan Welsch – Pionier, Stuntman, Mentor. Als Gründer von b-nova ist Stefan immer auf der Suche nach neuen und vielversprechenden Entwicklungsfeldern. Er ist durch und durch Pragmatiker und schreibt daher auch am liebsten Beiträge die sich möglichst nahe an 'real-world' Szenarien anlehnen.