Skip to content

Commit

Permalink
Merge pull request #47 from DopplerHQ/nic/secret-types
Browse files Browse the repository at this point in the history
Add support for custom managed secret types
  • Loading branch information
nmanoogian authored Aug 14, 2023
2 parents 1d18bce + 145f8d1 commit a7bc30c
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 85 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,14 @@ You can then configure your deployment spec to mount the file at the desired pat
path: appsettings.json # Name or path to file name appended to container mountPath
```

## Custom Value Encoding With Processors
## Kubernetes Secret Types and Value Encoding

By default, the operator syncs secret values as they are in Doppler to an [`Opaque` Kubernetes secret](https://kubernetes.io/docs/concepts/configuration/secret/) as Key / Value pairs. In some cases, the value stored in Doppler is not the format required for your Kubernetes deployment. For example, Base64 encoded `.p12` key file that needs to be decoded for mounting in a container in its original binary format. You can use [processors](docs/processors.md) to modify this behavior.
By default, the operator syncs secret values as they are in Doppler to an [`Opaque` Kubernetes secret](https://kubernetes.io/docs/concepts/configuration/secret/) as Key / Value pairs.

In some cases, the secret name or value stored in Doppler is not the format required for your Kubernetes deployment.
For example, you might have Base64-encoded TLS data that you want to copy to a native Kubernetes TLS secret (`kubernetes.io/tls`).

You can use [custom types and processors](docs/custom_types_and_processors.md) to achieve this.

## Failure Strategy and Troubleshooting

Expand Down
29 changes: 25 additions & 4 deletions api/v1alpha1/dopplersecret_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import (
// This file is meant to be modified as specs change.
// Important: Run "make" to regenerate code after modifying this file

// A reference to a Kubernetes secret
type SecretReference struct {
// A reference to a token Kubernetes secret
type TokenSecretReference struct {
// The name of the Secret resource
Name string `json:"name"`

Expand All @@ -35,10 +35,31 @@ type SecretReference struct {
Namespace string `json:"namespace,omitempty"`
}

// A reference to a managed Kubernetes secret
type ManagedSecretReference struct {
// The name of the Secret resource
Name string `json:"name"`

// Namespace of the resource being referred to. Ignored if not cluster scoped
// +optional
Namespace string `json:"namespace,omitempty"`

// The secret type of the managed secret
// +kubebuilder:validation:Enum=Opaque;kubernetes.io/tls;kubernetes.io/service-account-token;kubernetes.io/dockercfg;kubernetes.io/dockerconfigjson;kubernetes.io/basic-auth;kubernetes.io/ssh-auth;bootstrap.kubernetes.io/token
// +kubebuilder:default=Opaque
// +optional
Type string `json:"type,omitempty"`
}

type SecretProcessor struct {
// The type of process to be performed, either "plain" or "base64"
// +kubebuilder:validation:Enum=plain;base64
// +kubebuilder:default=plain
// +optional
Type string `json:"type"`

// The mapped name of the field in the managed secret, defaults to the original Doppler secret name for Opaque Kubernetes secrets. If omitted for other types, the value is not copied to the managed secret.
AsName string `json:"asName,omitempty"`
}

type SecretProcessors map[string]*SecretProcessor
Expand All @@ -48,10 +69,10 @@ var DefaultProcessor = SecretProcessor{Type: "plain"}
// DopplerSecretSpec defines the desired state of DopplerSecret
type DopplerSecretSpec struct {
// The Kubernetes secret containing the Doppler service token
TokenSecretRef SecretReference `json:"tokenSecret,omitempty"`
TokenSecretRef TokenSecretReference `json:"tokenSecret,omitempty"`

// The Kubernetes secret where the operator will store and sync the fetched secrets
ManagedSecretRef SecretReference `json:"managedSecret,omitempty"`
ManagedSecretRef ManagedSecretReference `json:"managedSecret,omitempty"`

// The Doppler project
// +optional
Expand Down
4 changes: 2 additions & 2 deletions api/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ limitations under the License.
*/

// Package v1alpha1 contains API Schema definitions for the secrets v1alpha1 API group
//+kubebuilder:object:generate=true
//+groupName=secrets.doppler.com
// +kubebuilder:object:generate=true
// +groupName=secrets.doppler.com
package v1alpha1

import (
Expand Down
23 changes: 19 additions & 4 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 20 additions & 2 deletions config/crd/bases/secrets.doppler.com_dopplersecrets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ spec:
description: Namespace of the resource being referred to. Ignored
if not cluster scoped
type: string
type:
default: Opaque
description: The secret type of the managed secret
enum:
- Opaque
- kubernetes.io/tls
- kubernetes.io/service-account-token
- kubernetes.io/dockercfg
- kubernetes.io/dockerconfigjson
- kubernetes.io/basic-auth
- kubernetes.io/ssh-auth
- bootstrap.kubernetes.io/token
type: string
required:
- name
type: object
Expand All @@ -80,15 +93,20 @@ spec:
processors:
additionalProperties:
properties:
asName:
description: The mapped name of the field in the managed secret,
defaults to the original Doppler secret name for Opaque Kubernetes
secrets. If omitted for other types, the value is not copied
to the managed secret.
type: string
type:
default: plain
description: The type of process to be performed, either "plain"
or "base64"
enum:
- plain
- base64
type: string
required:
- type
type: object
description: A list of processors to transform the data during ingestion
type: object
Expand Down
53 changes: 39 additions & 14 deletions controllers/dopplersecret_controller_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,9 @@ func GetDashboardLink(secrets []models.Secret) string {
}

// GetReferencedSecret gets a Kubernetes secret from a SecretReference
func (r *DopplerSecretReconciler) GetReferencedSecret(ctx context.Context, ref secretsv1alpha1.SecretReference) (*corev1.Secret, error) {
kubeSecretNamespacedName := types.NamespacedName{
Namespace: ref.Namespace,
Name: ref.Name,
}
func (r *DopplerSecretReconciler) GetReferencedSecret(ctx context.Context, namespacedName types.NamespacedName) (*corev1.Secret, error) {
existingKubeSecret := &corev1.Secret{}
err := r.Client.Get(ctx, kubeSecretNamespacedName, existingKubeSecret)
err := r.Client.Get(ctx, namespacedName, existingKubeSecret)
if err != nil {
existingKubeSecret = nil
}
Expand All @@ -83,7 +79,11 @@ func (r *DopplerSecretReconciler) GetReferencedSecret(ctx context.Context, ref s

// GetDopplerToken gets the Doppler Service Token referenced by the DopplerSecret
func (r *DopplerSecretReconciler) GetDopplerToken(ctx context.Context, dopplerSecret secretsv1alpha1.DopplerSecret) (string, error) {
tokenSecret, err := r.GetReferencedSecret(ctx, dopplerSecret.Spec.TokenSecretRef)
tokenSecretNamespacedName := types.NamespacedName{
Name: dopplerSecret.Spec.TokenSecretRef.Name,
Namespace: dopplerSecret.Spec.TokenSecretRef.Namespace,
}
tokenSecret, err := r.GetReferencedSecret(ctx, tokenSecretNamespacedName)
if err != nil {
return "", fmt.Errorf("Failed to fetch token secret reference: %w", err)
}
Expand All @@ -95,16 +95,26 @@ func (r *DopplerSecretReconciler) GetDopplerToken(ctx context.Context, dopplerSe
}

// GetKubeSecretData generates Kube secret data from a Doppler API secrets result
func GetKubeSecretData(secretsResult models.SecretsResult, processors secretsv1alpha1.SecretProcessors) (map[string][]byte, error) {
func GetKubeSecretData(secretsResult models.SecretsResult, processors secretsv1alpha1.SecretProcessors, includeSecretsByDefault bool) (map[string][]byte, error) {
kubeSecretData := map[string][]byte{}
for _, secret := range secretsResult.Secrets {
secretName := secret.Name

// Processors
processor := processors[secret.Name]
if processor == nil {
processor = &secretsv1alpha1.DefaultProcessor
}

var secretName string

if processor.AsName != "" {
secretName = processor.AsName
} else if includeSecretsByDefault {
secretName = secret.Name
} else {
// Omit this secret entirely
continue
}

processorFunc := procs.All[processor.Type]
if processorFunc == nil {
return nil, fmt.Errorf("Failed to process data with unknown processor: %v", processor.Type)
Expand Down Expand Up @@ -151,7 +161,11 @@ func GetProcessorsVersion(processors secretsv1alpha1.SecretProcessors) (string,

// CreateManagedSecret creates a managed Kubernetes secret
func (r *DopplerSecretReconciler) CreateManagedSecret(ctx context.Context, dopplerSecret secretsv1alpha1.DopplerSecret, secretsResult models.SecretsResult) error {
secretData, dataErr := GetKubeSecretData(secretsResult, dopplerSecret.Spec.Processors)
var includeSecretsByDefault bool
if dopplerSecret.Spec.ManagedSecretRef.Type == string(corev1.SecretTypeOpaque) {
includeSecretsByDefault = true
}
secretData, dataErr := GetKubeSecretData(secretsResult, dopplerSecret.Spec.Processors, includeSecretsByDefault)
if dataErr != nil {
return fmt.Errorf("Failed to build Kubernetes secret data: %w", dataErr)
}
Expand All @@ -168,7 +182,7 @@ func (r *DopplerSecretReconciler) CreateManagedSecret(ctx context.Context, doppl
"secrets.doppler.com/subtype": "dopplerSecret",
},
},
Type: "Opaque",
Type: corev1.SecretType(dopplerSecret.Spec.ManagedSecretRef.Type),
Data: secretData,
}
err := r.Client.Create(ctx, newKubeSecret)
Expand All @@ -181,7 +195,11 @@ func (r *DopplerSecretReconciler) CreateManagedSecret(ctx context.Context, doppl

// UpdateManagedSecret updates a managed Kubernetes secret
func (r *DopplerSecretReconciler) UpdateManagedSecret(ctx context.Context, secret corev1.Secret, dopplerSecret secretsv1alpha1.DopplerSecret, secretsResult models.SecretsResult) error {
secretData, dataErr := GetKubeSecretData(secretsResult, dopplerSecret.Spec.Processors)
var includeSecretsByDefault bool
if dopplerSecret.Spec.ManagedSecretRef.Type == string(corev1.SecretTypeOpaque) {
includeSecretsByDefault = true
}
secretData, dataErr := GetKubeSecretData(secretsResult, dopplerSecret.Spec.Processors, includeSecretsByDefault)
if dataErr != nil {
return fmt.Errorf("Failed to build Kubernetes secret data: %w", dataErr)
}
Expand Down Expand Up @@ -214,10 +232,17 @@ func (r *DopplerSecretReconciler) UpdateSecret(ctx context.Context, dopplerSecre
return fmt.Errorf("Failed to load Doppler Token: %w", err)
}

existingKubeSecret, err := r.GetReferencedSecret(ctx, dopplerSecret.Spec.ManagedSecretRef)
managedSecretNamespacedName := types.NamespacedName{
Name: dopplerSecret.Spec.ManagedSecretRef.Name,
Namespace: dopplerSecret.Spec.ManagedSecretRef.Namespace,
}
existingKubeSecret, err := r.GetReferencedSecret(ctx, managedSecretNamespacedName)
if err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("Failed to fetch managed secret reference: %w", err)
}
if existingKubeSecret != nil && existingKubeSecret.Type != corev1.SecretType(dopplerSecret.Spec.ManagedSecretRef.Type) {
return fmt.Errorf("Cannot change existing managed secret type from %v to %v. Delete the managed secret and re-apply the DopplerSecret.", existingKubeSecret.Type, dopplerSecret.Spec.ManagedSecretRef.Type)
}

currentProcessorsVersion, err := GetProcessorsVersion(dopplerSecret.Spec.Processors)
if err != nil {
Expand Down
74 changes: 74 additions & 0 deletions docs/custom_types_and_processors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Custom Types and Processors

By default, the operator syncs secret values as they are in Doppler to an [`Opaque` Kubernetes secret](https://kubernetes.io/docs/concepts/configuration/secret/) as Key / Value pairs.

In some cases, the secret name or value stored in Doppler is not the format required for your Kubernetes deployment.
For example, you might have Base64-encoded TLS data that you want to copy to a native Kubernetes TLS secret.

Processors provide a mechanism to achieve this.

Below is the Doppler Secret used in the Getting Started example with some modifications.

```yaml
apiVersion: secrets.doppler.com/v1alpha1
kind: DopplerSecret
metadata:
name: dopplersecret-test
namespace: doppler-operator-system
spec:
tokenSecret:
name: doppler-token-secret
managedSecret:
name: doppler-test-secret
namespace: default
type: kubernetes.io/tls
# TLS secrets are required to have the secret fields `tls.crt` and `tls.key`
processors:
TLS_CRT:
type: base64
asName: tls.crt
TLS_KEY:
type: base64
asName: tls.key
```
First, we've added a `type` field to the managed secret reference to define the `kubernetes.io/tls` managed secret type. When the operator creates the managed secret, it will have this Kubernetes secret type.

We've also added a field called `processors`. Processors can make alterations to a secret's name or value before they are saved to the Kubernetes managed secret.

Kubernetes TLS manged secrets require the `tls.crt` and `tls.key` fields to be present in the secret data. To accommodate this, we're using two processors to remap our Doppler secrets named `TLS_CRT` and `TLS_KEY` to the correct field names with `asName`.

We can define the processor's `type` to instruct the operator to transform the secret value before saving it into the managed secret. Processors have a default type of `plain`, which treats the Doppler secret value as a plain string. In our example, we've provided the `base64` type which instructs the operator to process the Doppler secret value as Base64 encoded data.

**Note:** The processors are only applied if there is a Doppler secret in your config which corresponds with the processor name.

You can have any number of processors, each with different types and name mappings (or no name mapping at all).

```yaml
processors:
MY_SECRET:
type: plain
OTHER_SECRET:
type: plain
asName: otherSecret
```

Below are the types of processors available to the operator:

## Plain

```yaml
type: plain
```

The default processor. This treats the data in the secret as plain string data.

## Base64

```yaml
type: base64
```

This processor will attempt to [Base64](https://en.wikipedia.org/wiki/Base64) decode the provided string and output the resulting bytes.

For example, the Base64 processor could be used to decode a Base64 encoded `.p12` file for mounting in a container in its original binary format.
Loading

0 comments on commit a7bc30c

Please sign in to comment.