A First Look at Helm 3

Helm has been widely publicized as the package manager for Kubernetes. We’ve seen the need over and over for Helm. Unfortunately, Helm 2 requires Tiller and Tiller opens a lot of security questions. In particular, in a multi-user, multi-organization, and/or multi-tenant cluster, securing the Tiller service account (or accounts) was difficult and problematic. As a result, we’ve never recommended our clients use Helm in production. With the recent announcement of the first release candidate for Helm 3, it’s time to take another look as this version no longer requires or uses Tiller so many (most) of our security concerns should be gone.

Installing Helm 3

Installing Helm 3 is easy. Download the release from GitHub, uncompress the tar/zip file and move the binary to someplace in your path.

$ wget https://get.helm.sh/helm-v3.0.0-rc.2-linux-amd64.tar.gz
$ tar xzf helm-v3.0.0-rc.2-linux-amd64.tar.gz
$ mv linux-amd64/helm ~/.bin/
$ helm version
version.BuildInfo{Version:"v3.0.0-rc.2", GitCommit:"82ea5aa774661cc6557cb57293571d06f94aff0c", GitTreeState:"clean", GoVersion:"go1.13.3"}

First Steps

In the past I’ve used the nfs-client-provisioner Helm chart, to create a default storage class for my test Kubernetes clusters. The first thing I ran into was the --name command line option is no longer valid. With Helm 3 you now have to use the form helm [command] [name] [chart].

$ helm install --name nfs-client-provisioner --set storageClass.defaultClass=true --set nfs.server=${FS_DNSNAME} --set nfs.path=/ stable/nfs-client-provisioner
Error: unknown flag: --name

The next thing I ran into is Helm 3 not being able to find my chart. Helm 3 stores the repository information in a separate (but now standard) location (under ~/.cache/helm/repository) so we have to do a repository update in order to find charts from the default repository (since there isn’t one now).

$ helm install --set storageClass.defaultClass=true --set nfs.server=${FS_DNSNAME} --set nfs.path=/ nfs-client-provisioner stable/nfs-client-provisioner
Error: failed to download "stable/nfs-client-provisioner" (hint: running `helm repo update` may help)
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈ Happy Helming!⎈
$ helm install --set storageClass.defaultClass=true --set nfs.server=${FS_DNSNAME} --set nfs.path=/ nfs-client-provisioner stable/nfs-client-provisioner
NAME: nfs-client-provisioner
LAST DEPLOYED: Sun Nov  3 18:00:37 2019
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

With Helm 2, you used the delete command with the --purge option to uninstall a chart and delete the history. With Helm 3, the delete command is an alias for the uninstall command and, by default, the history is deleted. (If you want to keep the history, you can use the --keep-history option.)

$ helm delete --purge nfs-client-provisioner
Error: unknown flag: --purge
$ helm uninstall nfs-client-provisioner
release "nfs-client-provisioner" uninstalled

Setup our test environment

In a past blog post, I setup Functional Kubernetes Namespaces in Docker Enterprise. We’ll do that again here and see how Helm 3 handles our previous security concerns. As a reminder, the development team has the edit ClusterRole for the development namespace and the view ClusterRole for the test and production namespaces. The test team has the view role for the development and production namespaces and the edit role for the test namespace. And, the operations team has the admin role in all of those namespaces. Finally, we have a user without any defined rolebindings.

Using Helm to deploy an application

In the following examples, we’re going to use the WordPress Helm chart. We’ll start by having our development, test and operations teams deploy the same application into the development, test, and production namespaces, respectively. Then, we’ll have our user with no privileges attempt to install it. To switch between users and namespaces, we’ll use the --namespace and --kubeconfig options, respectively, to the helm command.

$ helm install --namespace development --kubeconfig joe-dev/kube.yml wordpress stable/wordpress
NAME: wordpress
LAST DEPLOYED: Mon Nov  4 15:44:34 2019
NAMESPACE: development
STATUS: deployed
REVISION: 1
NOTES:
1. Get the WordPress URL:

  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
        Watch the status with: 'kubectl get svc --namespace development -w wordpress'
  export SERVICE_IP=$(kubectl get svc --namespace development wordpress --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}")
  echo "WordPress URL: http://$SERVICE_IP/"
  echo "WordPress Admin URL: http://$SERVICE_IP/admin"

2. Login with the following credentials to see your blog

  echo Username: user
  echo Password: $(kubectl get secret --namespace development wordpress -o jsonpath="{.data.wordpress-password}" | base64 --decode)
$ helm install --namespace test --kubeconfig jane-test/kube.yml wordpress stable/wordpress
NAME: wordpress
LAST DEPLOYED: Mon Nov  4 15:45:23 2019
NAMESPACE: test
STATUS: deployed
REVISION: 1
NOTES:
1. Get the WordPress URL:

  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
        Watch the status with: 'kubectl get svc --namespace test -w wordpress'
  export SERVICE_IP=$(kubectl get svc --namespace test wordpress --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}")
  echo "WordPress URL: http://$SERVICE_IP/"
  echo "WordPress Admin URL: http://$SERVICE_IP/admin"

2. Login with the following credentials to see your blog

  echo Username: user
  echo Password: $(kubectl get secret --namespace test wordpress -o jsonpath="{.data.wordpress-password}" | base64 --decode)
$ helm install --namespace production --kubeconfig pat-ops/kube.yml wordpress stable/wordpress
NAME: wordpress
LAST DEPLOYED: Mon Nov  4 15:46:03 2019
NAMESPACE: production
STATUS: deployed
REVISION: 1
NOTES:
1. Get the WordPress URL:

  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
        Watch the status with: 'kubectl get svc --namespace production -w wordpress'
  export SERVICE_IP=$(kubectl get svc --namespace production wordpress --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}")
  echo "WordPress URL: http://$SERVICE_IP/"
  echo "WordPress Admin URL: http://$SERVICE_IP/admin"

2. Login with the following credentials to see your blog

  echo Username: user
  echo Password: $(kubectl get secret --namespace production wordpress -o jsonpath="{.data.wordpress-password}" | base64 --decode)
$ helm install --namespace default --kubeconfig arjun-nopriv/kube.yml wordpress stable/wordpress
Error: rendered manifests contain a resource that already exists. Unable to continue with install: could not get information about the resource: secrets "wordpress-mariadb" is forbidden: User "13607232-0375-4118-b20e-3ba6bb935f40" cannot get resource "secrets" in API group "" in the namespace "default": access denied

The first thing you will notice is that users from each of the teams could deploy the application in their respective namespace. Second, you may have noticed I used the same name for each of the installs. Names only have to be unique within a namespace not across all namespaces. Third, the user with no privileges couldn’t install the chart, even in the default namespace.

Viewing our Helm deployments

Now, let’s see what each of these teams can see. You’ll notice the development user can only see the charts installed in the development namespace. And, the operations user can see all of the installed charts (using the new --all-namespaces option in the second release candidate).

$ helm list --namespace production --kubeconfig joe-dev/kube.yml
NAME    NAMESPACE       REVISION        UPDATED STATUS  CHART   APP VERSION
$ helm list --namespace test --kubeconfig joe-dev/kube.yml
NAME    NAMESPACE       REVISION        UPDATED STATUS  CHART   APP VERSION
$ helm list --namespace development --kubeconfig joe-dev/kube.yml
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART          APP VERSION
wordpress       development     1               2019-11-04 15:44:34.300900099 +0000 UTC deployed        wordpress-7.6.15.2.4
$ helm list --all-namespaces --kubeconfig pat-ops/kube.yml
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART          APP VERSION
wordpress       development     1               2019-11-04 15:44:34.300900099 +0000 UTC deployed        wordpress-7.6.15.2.4
wordpress       test            1               2019-11-04 15:45:23.827980207 +0000 UTC deployed        wordpress-7.6.15.2.4
wordpress       production      1               2019-11-04 15:46:03.701766913 +0000 UTC deployed        wordpress-7.6.15.2.4

Trying cross functional deployments

Next, let’s see what a user can do in a namespace for which they don’t have the necessary privileges to deploy an application. In this case, we’ll have a development user attempt to deploy an application in the production namespace.

$ helm install --namespace production --kubeconfig joe-dev/kube.yml dev-wordpress stable/wordpress
Error: rendered manifests contain a resource that already exists. Unable to continue with install: could not get information about the resource: secrets "dev-wordpress-mariadb" is forbidden: User "6dda6713-6aa5-474f-93be-c8bb50cafeef" cannot get resource "secrets" in API group "" in the namespace "production": access denied

The development user doesn’t have permission to get secrets in the production namespace and Helm 3 uses secrets (rather than Tiller) to manage chart installs in the namespace. As a result, the development user can’t install a chart in the production namespace. (Of course, we could give the development team the ability to manage secrets in the production namespace but we’d be back to our original security concerns.)

Helm Secrets

If you can’t get secrets in a namespace, you can’t list (or install or uninstall) charts installed in that namespace. As a cluster admin, we can see the secrets Helm 3 created in each of the namespaces where charts have been installed.

$ kubectl get secrets --all-namespaces | grep helm
NAMESPACE         NAME                                             TYPE                                  DATA   AGE
development       sh.helm.release.v1.wordpress.v1                  helm.sh/release.v1                    1      15m
production        sh.helm.release.v1.wordpress.v1                  helm.sh/release.v1                    1      13m
test              sh.helm.release.v1.wordpress.v1                  helm.sh/release.v1                    1      14m

$ kubectl describe secret sh.helm.release.v1.wordpress.v1 -n development
Name:         sh.helm.release.v1.wordpress.v1
Namespace:    development
Labels:       modifiedAt=1572882276
              name=wordpress
              owner=helm
              status=deployed
              version=1
Annotations:  <none>

Type:  helm.sh/release.v1

Data
====
release:  33836 bytes

$ kubectl get secret sh.helm.release.v1.wordpress.v1 -n development -o json | jq -r .data.release | base64 --decode -
H4sIAAAAAAAC/+y9CXOj...

Even though we can (as a cluster admin) decode the secrets, we’re still left with a non-text string so there’s not a lot of information we can get from it. (Maybe someone has a way to further “decode” this secret but I haven’t run across it yet.)

Summary

We’ve now taken our first look at Helm 3. It appears as if it has addressed the security problems we’ve seen in the past with Tiller. We’ll be doing more testing with Helm as a result. A lot is going on in this space. If you want or need help, Capstone IT is a Docker Premier Consulting Partner as well as being an Azure Gold and AWS Select partner. If you are interested in finding out more and getting help with your Container, Cloud and DevOps transformation, please Contact Us.

Ken Rider
Solutions Architect
Capstone IT