Using kyverno to generate secrets
Sometimes it's the little helpers that make the difference. Here's some kyverno policy you can use to generate secret values in Kubernetes through a mutating webhook.
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.