Using kyverno to generate secrets

Working heavily with helm charts, sometimes you face the situation that you need to create passwords dynamically and you don't want (or can) use external tooling. A simple situation could be database users for databases inside the cluster, a password for a redis database used a cache or just some credential for a cluster scope oci registry.


To solve this, we're simply using a kyverno policy which helps to generate passwords dynamically. To make sure passwords are static (once assigned) you don't want to generate them using helm and you for sure do not want to provide them through helm values.


The usage is simple, just use annotations:

apiVersion: v1
kind: Secret
metadata:
  annotations:
    secretgenerator.nuvotex.io/clusteruser: preset=default,length=32
  name: oci-registry-credentials
  namespace: oci-registry
type: Opaque

usage of the policy

Adding this annotation will - using a mutating webhook - inject (if not present) the field clusteruser with a dynamically generated password with a length of 32 characters. The default preset in this case will generate alphaNumeric passwords, so that you also don't have escaping issues which you can face when having too complex characters. If you need more entropy, you can provide another preset, just keep it simple and pragmatic.

If the secret will be stored (for example when editing the secret) and the clusterluser is already assiged, the previous value will be kept (and not validated against the preset). Additional fields will also remain untouched.
This is especially awesome when you combine it with externalsecrets operator which you can use to template for example more complex configurations (like redis config) into another secret, so that you can derive the usages from this generated secret.

Here's the policy you can use to add this to your cluster.

# Source: kyverno-policies/templates/generateSecret.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-secret
spec:
  background: true
  rules:
    - name: preset-default
      match:
        resources:
          kinds:
            - Secret
      mutate:
        foreach:
        - list: "items(request.object.metadata.annotations || `{}`,'key','value')"
          context:
            - name: field
              variable:
                jmesPath: "element.key | trim_prefix(@, 'secretgenerator.nuvotex.io/')"
            - name: length
              variable:
                jmesPath: "element.value | regex_replace_all('(^|.+,)length=(\\d+)(,.+|$)', @, '${2}')"
                default: 32
            - name: preset
              variable:
                jmesPath: "element.value | regex_replace_all('(^|.+,)preset=([\\d\\w]+)(,.+|$)', @, '${2}')"
                default: null
            - name: secret
              variable:
                # maybe someone smart finds a way to do this without truncate and just with random directly
                jmesPath: "random('[A-Za-z0-9]{512}') | truncate(@, `{{length}}`) | base64_encode(@)"
          preconditions:
            all:
            - key: "{{ element.key }}"
              operator: Equals
              value: "secretgenerator.nuvotex.io/*"
            any:
            - key: "{{ preset }}"
              operator: Equals
              value: "default"
          patchStrategicMerge:
            data:
              +({{ field }}): "{{ secret }}"

generate secret policy

Simple. Awesome.