Managing secrets during deployment in k8s

Hello, habr!

At a certain point, we come to understand that a process that works “well” must start working “correctly”, especially when it comes to application secrets.

Given: gitlab(onprem), cloud (in my case Yandex Cloud), 10+ services that need to be actively and frequently deployed (with the ability to quickly increase the number of services)

Requirements for the solution: deployments must occur without the intervention of an engineer directly during the deployment process, the process is transparent to the developer and meets the company’s quality and safety requirements.


Solution: Let me make a reservation right away – my solution is built on Yandex Cloud, but it is valid for any cloud or onprem solution.
I pass the path from the file deployment locally through kubectl to Helm (possibly in a separate topic)

So there are repositories with code, we place . {environment}.values.yaml
When deploying, specify –values=/path/to/file

The main question arises – if the configmap is stored in the repository (via helm variables – this is correct), then what to do with the secrets?

Option 1: use helm-secrets, sops and other solutions
We receive encrypted files with secrets in the repository. When deploying, we specify the path to the file, helm decrypts and deploys the application along with the secrets. However, overhead immediately comes in the form of a system for transferring the key with which you encrypted, be sure not to forget to add *.dec to gitignore, otherwise at some point the secrets leak into the repository and what was the point then? Well, there is a great risk of losing the key and secrets at the same time. The main disadvantage of the approach is that when you change the secret, you need to go through the entire pipeline again, which means checks, code validation, and tests take a long time.
Option 2: use secret management through external systems.
Additionally we will need:

The choice of secret storage falls on the shoulders of the systems engineer or architect. I can tell you from experience – I used vault and lockbox. The first one is, of course, more powerful, but for a small enterprise the second solution is enough.

Next, we deploy External Secret Operator in the cluster

Create a service account with a role

lockbox.editor

export HELM_EXPERIMENTAL_OCI=1 && \
helm pull oci://cr.yandex/yc-marketplace/yandex-cloud/external-secrets/chart/external-secrets \
  --version 0.5.5 \
  --untar && \
helm install \
  --namespace external-secret-operator \
  --create-namespace \
  --set-file auth.json=sa-key.json \
  external-secrets ./external-secrets/

To work with secrets, we will also deploy secretstore. It is worth noting that secretstore works in namespace mode and isolates secrets within one ns, ClusterSecretStore allows you to share secrets between different ns. This should be remembered and not once again fumbling around for secrets that will not be used in two or more ns.

First create a secret (in the example yc-auth) with the keys from the service account created earlier. Then set secretstore to the desired namespace

apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
  name: secret-store
spec:
  provider:
    yandexlockbox:
      auth:
        authorizedKeySecretRef:
          name: yc-auth
          key: authorized-key'

At this stage, the preparatory work has been completed.
You can add a template for external-secret to the application chart (or change the example to use it like a regular manifest)


apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
  namespace: {{.Values.namespace }}
  name: {{ include "helm-chart.name" . }}
spec:
  refreshInterval: {{ .Values.secretRefreshInterval }}
  secretStoreRef:
    name: secret-store
    kind: SecretStore
  target:
    name: {{ include "helm-chart.name" . }}
  data:
  - secretKey: DATABASE_URL
    remoteRef:
      key: {{ .Values.secretRemote }}
      property: DATABASE_URL

In this example, an external-secret resource is created, which is named after the name of the chart, causing the creation of a k8s secret of the same name from the external secret.
Let's pay attention to two parameters:
secretRefreshInterval — interval with which the secret-operator will poll the lockbox to change the secret (default 1h0m0s)
secretRemote – secret identifier in lockbox (the latest current version of the secret will be captured)

After applying the manifest, make sure the resource is in synced state

At this point, we received managed secrets in k8s, we change them in the lockbox – after some time – they change them in Kubernetes. But this is not enough, the deployment will not know that you have changed the secret or added a new variable to it.
This is where the Kyverno tool comes into the picture (if necessary, I will write a separate article about setting it up). For now, let's assume that you already have it.


apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: deployment-restart-policy
  namespace: test-ns
  annotations:
    policies.kyverno.io/title: Restart Deployment On Secret Change
    policies.kyverno.io/category: Other
    policies.kyverno.io/severity: medium
    policies.kyverno.io/subject: Deployment
    kyverno.io/kyverno-version: 1.9.0
    policies.kyverno.io/minversion: 1.7.0
    kyverno.io/kubernetes-version: "1.27"
spec:
  schemaValidation: false
  rules:
  - name: update-secret
    match:
      any:
      - resources:
          kinds:
          - Secret
          names:
          - test-database-adapter
          namespaces:
          - sup
    preconditions:
      all:
      - key: "{{request.operation || 'BACKGROUND'}}"
        operator: Equals
        value: UPDATE
    mutate:
      targets:
        - apiVersion: apps/v1
          kind: Deployment
          name: test-database-adapter
          namespace: test-ns
      patchStrategicMerge:
        spec:
          template:
            metadata:
              annotations:
                ops.corp.com/triggerrestart: "{{request.object.metadata.resourceVersion}}"

This policy does not claim to be a standard, but it fulfills its task – namely, to restart the deployment if the checksum of the specified secret changes.

As a result, what solution did we get:
When the application is deployed, an external secret is created, which is responsible for updating the k8s secret. Changing the latter serves as a trigger to restart the application.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *