Bob's Journey Continues: Build a Secure Chart

Join Bob and Josie as they build a secure Helm chart from the ground up, exploring Kubernetes best practices and mastering advanced Helm configurations

Bob's Journey Continues: Build a Secure Chart

Bob, eager to build his Helm chart, turns to Josie. "Alright, Josie, I'm ready to build that chart for my To-Do List application. Where do we start?"

Josie smiles. "Great! Let's create a secure Helm chart for your To-Do List application from scratch. I'll guide you through each step."

📂 Code Repository: Explore the complete code and configurations for this article on GitHub.

View Repository on GitHub

1. Setting Up the Chart Structure

"First, we need to create a directory for our chart and initialise it," Josie explains.

$ helm create my-todo-list
Creating my-todo-list
$ cd my-todo-list
$ code .

"This creates the basic Helm chart structure" she adds, launching VSCode "we'll now populate it with our application's specifics." A quick inspection shows the following structure:

$ tree .
.
├── charts
├── Chart.yaml
├── dryrun.yaml
├── summary.txt
├── templates
│   ├── deployment.yaml
│   ├── _helpers.tpl
│   ├── _hpa.yaml
│   ├── ingress.yaml
│   ├── NOTES.txt
│   ├── serviceaccount.yaml
│   └── service.yaml
└── values.yaml

3 directories, 11 files
💡
Note:

To keep helm charts DRY we can leverage the _helpers.tpl file to keep a track of vaiables and use those throughout the templates.

2. Cleaning Up

"Let's delete the charts folder as we have no chart dependencies" Josie says as they flick through the templates folder. "We only have a single host, so let's simplify things."

3. Update Chart.yaml

Bob opens the Chart.yaml file and fills in the necessary information:

apiVersion: v2
name: my-todo-list
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: "1.0.0"
maintainers:
- name: Bob
  email: bobs_email@example.com

4. Craft the Ingress

Bob opens the templates/ingress.yaml file and modifies it as follows to expose his application:

{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "my-todo-list.fullname" . }}
  labels:
    {{- include "my-todo-list.labels" . | nindent 4 }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  ingressClassName: {{ .Values.ingress.className | default "" }}
  tls:
    - hosts:
        - {{ .Values.ingress.host }}
      secretName: {{ include "my-todo-list.fullname" . }}-tls
  rules:
    - host: {{ .Values.ingress.host }}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: {{ include "my-todo-list.fullname" . }}
                port:
                  number: {{ .Values.service.port }}
{{- end }}

"This Ingress definition configures Traefik to route traffic to your To-Do List application," Josie explains. "The annotations ensure that HTTPS is enabled and that certificates are automatically provisioned using Let's Encrypt."

In the values.yaml file, Josie helps Bob to modify the ingress as follows:

ingress:
  enabled: true
  className: traefik
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
    cert-manager.io/cluster-issuer: letsencrypt-issuer
  host: my-todo-list.example.com

5. Expose the Service

Bob opens the templates/service.yaml file and modifies it as follows:

apiVersion: v1
kind: Service
metadata:
  name: {{ include "my-todo-list.fullname" . }}
  labels:
    {{- include "my-todo-list.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: {{ .Values.service.targetPort }}
      protocol: TCP
      name: http
  selector:
    {{- include "my-todo-list.selectorLabels" . | nindent 4 }}

He then modifies the values.yaml file as follows:

service:
  type: ClusterIP
  port: 80
  targetPort: 3000

6. Define the Service Account

"Now, let's define the service account that your application will eventually use," Josie says.

Bob creates the templates/serviceaccount.yaml file:

{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ include "my-todo-list.serviceAccountName" . }}
  labels:
    {{- include "my-todo-list.labels" . | nindent 4 }}
  {{- with .Values.serviceAccount.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
automountServiceAccountToken: false
{{- end }}

"This service account will be used by your application to interact with the Kubernetes API and other pods" Josie explains. "We've set automountServiceAccountToken to false for enhanced security, as your application doesn't currently require access to the API or any other pods at the moment."

7. Remove the HPA

"We'll cover this later as we grow your app" Josie says, and they delete the templates/hpa.yaml file.

8. Craft the Deployment

"Now for the fun bit, let's define your application's deployment in the templates/deployment.yaml file" Josie says.

Together, they craft the deployment definition, ensuring it aligns with security best practices:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-todo-list.fullname" . }}
  labels:
    {{- include "my-todo-list.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "my-todo-list.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      {{- with .Values.pods.annotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      labels:
        {{- include "my-todo-list.labels" . | nindent 8 }}
        {{- with .Values.pods.labels }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "my-todo-list.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.pods.securityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
              protocol: TCP
          {{- range .Values.probes }}
          {{- if .enabled }}
          {{ .name }}:
            {{- toYaml .probe | nindent 12 }}
          {{- end }}
          {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

"We've added security contexts at both the pod and container levels" Josie points out. "This enforces running the application as a non-root user and restricts privilege escalation, enhancing the security of your deployment."

9. Set Default Values

"Now, let's set some default configuration values in the values.yaml file" Josie says.

replicaCount: 1

image:
  repository: my.registry.example.com/bob/my-todo-list
  pullPolicy: IfNotPresent
  tag: 1.0.0

imagePullSecrets:
  - name: imagecredentials-secret

# This is to override the chart name.
nameOverride: ""
fullnameOverride: ""

serviceAccount:
  create: true

pods:
  annotations:
    owner: bobs_email@example.com
  labels: {}
  securityContext:
    runAsUser: 1001
    runAsGroup: 1001
    fsGroup: 1001

securityContext:
  capabilities:
    drop:
    - ALL
  readOnlyRootFilesystem: true
  runAsNonRoot: true
  runAsUser: 1001
  runAsGroup: 1001

service:
  type: ClusterIP
  port: 80
  targetPort: 3000

ingress:
  enabled: true
  className: traefik
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
    cert-manager.io/cluster-issuer: letsencrypt-issuer
  host: my-todo-list.example.com

resources: {}
  # limits:
  #   cpu: 100m
  #   memory: 128Mi
  # requests:
  #   cpu: 100m
  #   memory: 128Mi

probes:
  - name: startupProbe
    enabled: true
    probe:
      httpGet:
        path: /
        port: http
  - name: livenessProbe
    enabled: true
    probe:
      httpGet:
        path: /
        port: http
  - name: readinessProbe
    enabled: true
    probe:
      httpGet:
        path: /
        port: http

10. Lint, Dry-run and Scan

Bob remembers the earlier discussion and jumps into his terminal and runs the following ... grinning at the output 😁

$ helm lint
==> Linting .
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed

Josie spots his grin and say "Well done ... there's usually a lot more issues than this, now let's dry run it and scan it using Trivy". Bob taps out the following:

$ helm install todo-app . -f values.yaml --dry-run > dryrun.yaml
$ trivy config dryrun.yaml > summary.txt
2024-11-26T10:06:15Z    INFO    [misconfig] Misconfiguration scanning is enabled
2024-11-26T10:06:17Z    INFO    Detected config files   num=1

They both look through the output of the summary and spot a couple of required changes to their chart:

  1. MEDIUM: Container 'my-todo-list' of Deployment 'todo-app-my-todo-list' should set 'securityContext.allowPrivilegeEscalation' to false
  2. LOW: Container 'my-todo-list' of Deployment 'todo-app-my-todo-list' should set 'resources.limits|requests.memory|cpu'
  3. LOW: Container 'my-todo-list' of Deployment 'todo-app-my-todo-list' should set 'securityContext.runAsUser' > 10000
  4. LOW: Either Pod or Container should set 'securityContext.seccompProfile.type' to 'RuntimeDefault'

Josie remarks "This is looking pretty good for a first time run but there are a couple of tweaks we can make based on this information. First let's modify out values.yaml file and include the allowPrivilegeEscalation and the seccompProfile.type setting to the container securityContext settings. Also, given the app runs as user/group 1001 for now we'll leave the runAs settings as they are but in the future we can modify our Dockerfile to take this into account". Together they modify the values.yaml file as follows:

securityContext:
  capabilities:
    drop:
    - ALL
  readOnlyRootFilesystem: true
  runAsNonRoot: true
  runAsUser: 1001
  runAsGroup: 1001
  allowPrilegeEscalation: false
  seccompProfile:
    type: RuntimeDefault

Bob then regenerates the dryrun and rescans the chart while commenting "we don't know what the resource requirements are just yet so it should be okay to accept that one for now" as he makes a note to follow up on this later.

They double check the output and note the only remaining advisors are around the resources and the user/group being greater than 10000.

💡
Reminder:

It's worth noting that you don't have to resovle everything providing you understand why

6. Deploy and Verify the app

"Finally, we're ready to deploy our app into the cluster" Josie concludes. "Want to do the honours?" she asks Bob, he eagerly nods and types out:

$ helm install --namespace=mytodoapp todo-app . -f values.yaml
NAME: todo-app
LAST DEPLOYED: Tue Nov 26 10:22:28 2024
NAMESPACE: bobsjourney
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace mytodoapp -l "app.kubernetes.io/name=my-todo-list,app.kubernetes.io/instance=todo-app" -o jsonpath="{.items[0].metadata.name}")
  export CONTAINER_PORT=$(kubectl get pod --namespace mytodoapp $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
  echo "Visit https://my-todo-list.example.com to use your application"
  kubectl --namespace mytodoapp port-forward $POD_NAME 3000:$CONTAINER_PORT

Bob then checks the status of the pods, svc and ingress to make sure everything is present and expected:

$ kubectl get pods,svc,ingress
NAME                                     READY   STATUS    RESTARTS   AGE
pod/todo-my-todo-list-79cc46998d-7tj7w   1/1     Running   0          75s

NAME                        TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)                      AGE
service/todo-my-todo-list   ClusterIP      10.101.125.144   <none>          80/TCP                       2m7s

NAME                                          CLASS     HOSTS                      ADDRESS         PORTS     AGE
ingress.networking.k8s.io/todo-my-todo-list   traefik   my-todo-list.example.com   192.168.0.180   80, 443   39s

Seeing everything looks to be fine, he fires up a browser and goes to https://my-todo-list.example.com and jumps in excitement as he sees his example app loading.

Success!

Bob's To-Do List application is now deployed using his secure custom Helm chart! He's thrilled with the ease and security of the process.

"This is fantastic!" he exclaims. "Deploying my application securely was a breeze with Helm."

Josie smiles. "You've done a great job, Bob! You've built a secure Helm chart from scratch."

Next Steps

Bob, eager to learn more, asks, "What else can I do with Helm charts?"

"The possibilities are endless," Josie replies. "You can explore Helm's templating language to customise deployments further, use Helm hooks to execute jobs before or after deployment, and even create your own Helm repository to share your charts with others."

Bob's first custom Helm chart is a success! Not only has he securely deployed his To-Do List application, but he's also gained valuable insights into Helm's templating, security best practices, and deployment workflows.

As he starts thinking about future enhancements, such as integrating Redis for data persistence or automating more aspects of his deployments, Bob feels confident that Helm is the right tool to help him navigate the complexities of Kubernetes.

Stay tuned as Bob and Josie continue their journey, diving into advanced Helm features and tackling the challenges of scaling and optimisation!