Skip to content

Automatically Synchronize, Copy, and Reformat Kubernetes Secrets

License

Notifications You must be signed in to change notification settings

meln5674/secrets-operator

Repository files navigation

Secrets Operator

This tool follows the Kubernetes Operator Pattern, and is used to synchronize Secrets.

This tool is still in early development, and experimental. Everything is subject to change. Use at your own risk.

Motivation

Consider the following use case: You have an operator that is going to deploy a database, and create a Secret with the username and password to access it. Next, you use a Helm chart to deploy an application that will consume that Secret to connect to the database, but, horror of horrors, that Helm chart wants different keys to be in that Secret than the database operator will produce! Even worse, if that Helm chart is particularly brain-dead, it may require that secret to even have a specific name! Tragedy! You'll have to manually copy that secret and correct it for the Helm chart to accept it.

Not to worry! Simply create a DerivedSecret resource to automatically generate a Secret that meets the picky Helm chart's demands by synchronizing it with whatever Secret is generated by the operator!

Some likewise brain-dead operators will only let you create their custom resources in the same namespace as the operator themselves, irritating if you'd like to consume the Secrets generated by those custom resources in applications in other namespaces. Just as before, a DerivedSecret will fix you right up, as you can target the generated secret to the correct namespace.

Custom Resources

This operator provides the DerivedSecret resource. This represents a desired Secret that is based on one or more other Secrets and/or ConfigMaps, called "references". You can simply copy fields from references, or you can use Golang templates (with Sprig functions) to create entirely new values. Values that should be set once and only once (e.g. randomly generated) can be set to not overwrite upon update. Two additions functions "b64bin" and "utf8" are also included for handling binary data from Secret.data and ConfigMap.binaryData.

apiVersion: secrets.meln5674.github.com/v1alpha1
kind: DerivedSecret
metadata:
  name: my-derived-secret # By default, the generated secret will have the same name
spec:
  # This is the list of ConfigMaps and Secrets you can reference
  references:
  - name: myReference # Name can be anything, but valid go identifies are recommended for reasons below
    configMapRef:
      name: my-config-map
    # Or
    secretRef:
      name: my-secret
  # If you just want to copy a set of fields, you can use the prefab section
  prefab:
    # Copy every field from every reference, failing on duplicate keys
    copyAll: true
    # Or
    # Copy every field, except the ones listed
    copyExcluding:
    - name: myReference
      # Omit to exclude every key in the reference, or specify a subset
      keys: [not,these,keys]
    # Or
    # Copy just the fields listed
    copyIncluding:
    - name: myReference
      # Omit to include every key in the reference, or specify a subset
      keys: [just,these,keys]
  # Or, if you need fine-grained control
  stringData:
    # Encode literal data
    my-literal-key:
      literal: 'SHH! It's a secret!'
    # Or, apply a template to keys from references
    my-template-key:
      template: '{{ .References.myReference.key }}'
      # If you're including this resource in a helm chart, make sure to escape the template
      template: '{{ "{{ .References.myReference.key }}" }}'
      # If your key is binary data (e.g. Secret.data or ConfigMap.binaryData,
      # Sprig's b64enc will not work, as it expects a string
      # The b64bin function works like b64enc, but accepts bytes instead
      template: '{{ .References.myReference.binaryKey | b64bin | b64dec }}'
      # The utf8 function simply re-interprets a binary field as a utf-8 encoded string
      # (the default encoding for Golang), which acheives the same thing
      template: '{{ .References.myReference.binaryKey | utf8 }}'
      # If you need to produce multiple keys from the same template, such as when using sprig's
      # genCA function to randomly generate both a private key and certificate,
      # Set this to true to instead interpret the output of the template as a YAML document
      # Containing the actual values.
      # In this case, the key of this element is ignored, and only used when reporting errors.
      isMap: false
  # Also works with binary (Base64-encoded) data
  data:
    # We can safely use random functions from sprig with the 'overwrite: false' field
    my-random-secret:
      overwrite: false
      template: '{{ randBytes 32 | b64enc }}'
    # Binary literals are also supported by using base-64 encoding like in
    # Secret.data or ConfigMap.binaryData
    another-literal-key:
      literal: 'VGhpcyBpcyBhIHNlY3JldCwgd2hhdCBhcmUgeW91IGRvaW5nIGxvb2tpbmcgYXQgaXQ/Cg=='

  # If you need a different secret name, here's how to set it
  secretName: some-other-secret-name 

  # You can also target different namespaces
  secretNamespace: some-other-namespace
  # However, this raises security concerns. In this case, you will need to provide the name of a
  # ServiceAccount in the same namespace as the DerivedSecret to perform this action,
  # just to make sure you aren't doing something you're not allowed to
  # Make sure that the operator has permissions to impersonate this ServiceAccount
  serviceAccoutName: secrets-creator

Building

Requires:

  • Go 1.17
  • Make
  • Docker
make build docker-build

Installing

Requires:

  • Make
  • Kubectl
  • Kustomize
  • A working Kubernetes cluster
# Uses kustomize
make install deploy

Running Tests

Requires:

  • Go 1.17
  • Make
  • Kubectl
  • Kustomize
  • Krew (Installs Kuttl)
  • KinD
make test
# Requires krew, kubectl, 
make kuttl-tests

A Theory of Secrets

The Author would like to propose the following conceptual framework for handling secrets within Kubernetes secrets, or IT in general.

There are two types of secrets: Connective and Generative. A Connective secret is defined by a differenty party than the one using it. This could be a database password, an API token, or any sensitive information that another party provides in order to authenticate a potential consumer. A Generative secret, then is one which is defined by the party using it. The secret could then be Connective or Generative based on which perspective you take; a datbase password is Generative to the database that defines it. Treating these two types of secrets is a mistake, as they are fundamentally different.

Generative secrets should, as the name implies, be randomly generated, using a cryptographically secure method, on a once-and-only-once basis. If the secret exists, it should be assumed that it is still valid, which acheives idempotency. If the secret must be rotated, such as a certificate private key, then that should be accomplished by removing the original one, and re-running whatever process generated it in the first place. Connective secrets, on the other hand, should be automatically synchronized from the Generative secret that it is based on. Any system which does not do this runs the risk of breaking that connection, leading to downtime or improper operation. The logical conclusion of this line of reasoning is that the secrets themselves are immaterial. A system should, ideally, be represented statically without any concern to the actual state of any sensitive information it relies on. These secrets are a reflection of a higher level concept. Ideally, it should be possible to encode this directly, declaratively, rather than manually implementing it by manually managing these secrets. This leads to the subject of GitOps.

Within the practice of GitOps, the author has seen three different schools of thought:

  1. Randomly generate a secret on the first update, manually provide the generated secret back into the update process. (Most helm charts offer this)
  2. Encrypt secrets, and use a master secret to automatically decrypt them when they are updated. (e.g. Sealed Secrets or SOPS)
  3. Provide secrets "out of band" (i.e. manually with kubectl).

The author believes none of these are correct. The first not only creates a risk of data loss, but it destroys any hope of idempotency, which is highly desirable. The second and third do not have this risk, but lose the desirable property of correctly handling Generative secrets, and the second in particular necessitates providing an insecure default value. The third at least has the property that it will fail to even start if forgotten, unlike the second, and lends itself towards automatic generation, but none of these three methods account for the correct method of handling Connective secrets.

This, the author believes, is where this tool can solve this problem. This tool allows providing not secrets, but a policy of secrets. Generative secrets are correctly handled by using one of the sprig template functions such as genPrivateKey or randBytes along with overwrite: false, and Connective secrets are handled by creating new secrets based on the original Generative secrets. Because DerivedSecret resources contain no sensitive information themselves, they can be incorporated into GitOps with zero risk of accidentally commiting secrets.

About

Automatically Synchronize, Copy, and Reformat Kubernetes Secrets

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages