diff --git a/README.md b/README.md index c64182f..9cbae3f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/api/v1alpha1/dopplersecret_types.go b/api/v1alpha1/dopplersecret_types.go index bc833d7..a5a0ee4 100644 --- a/api/v1alpha1/dopplersecret_types.go +++ b/api/v1alpha1/dopplersecret_types.go @@ -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"` @@ -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 @@ -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 diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index abd2f1a..1c62b5c 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -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 ( diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 5f005ae..ba5cfbc 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -139,6 +139,21 @@ func (in *DopplerSecretStatus) DeepCopy() *DopplerSecretStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedSecretReference) DeepCopyInto(out *ManagedSecretReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedSecretReference. +func (in *ManagedSecretReference) DeepCopy() *ManagedSecretReference { + if in == nil { + return nil + } + out := new(ManagedSecretReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretProcessor) DeepCopyInto(out *SecretProcessor) { *out = *in @@ -184,16 +199,16 @@ func (in SecretProcessors) DeepCopy() SecretProcessors { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SecretReference) DeepCopyInto(out *SecretReference) { +func (in *TokenSecretReference) DeepCopyInto(out *TokenSecretReference) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretReference. -func (in *SecretReference) DeepCopy() *SecretReference { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenSecretReference. +func (in *TokenSecretReference) DeepCopy() *TokenSecretReference { if in == nil { return nil } - out := new(SecretReference) + out := new(TokenSecretReference) in.DeepCopyInto(out) return out } diff --git a/config/crd/bases/secrets.doppler.com_dopplersecrets.yaml b/config/crd/bases/secrets.doppler.com_dopplersecrets.yaml index 7453ce8..1e50743 100644 --- a/config/crd/bases/secrets.doppler.com_dopplersecrets.yaml +++ b/config/crd/bases/secrets.doppler.com_dopplersecrets.yaml @@ -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 @@ -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 diff --git a/controllers/dopplersecret_controller_secrets.go b/controllers/dopplersecret_controller_secrets.go index 5db711a..9b0cabe 100644 --- a/controllers/dopplersecret_controller_secrets.go +++ b/controllers/dopplersecret_controller_secrets.go @@ -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 } @@ -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) } @@ -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) @@ -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) } @@ -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) @@ -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) } @@ -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 { diff --git a/docs/custom_types_and_processors.md b/docs/custom_types_and_processors.md new file mode 100644 index 0000000..e84e03f --- /dev/null +++ b/docs/custom_types_and_processors.md @@ -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. diff --git a/docs/processors.md b/docs/processors.md deleted file mode 100644 index 4a45d98..0000000 --- a/docs/processors.md +++ /dev/null @@ -1,57 +0,0 @@ -# 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 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. - -Processors provide a mechanism to achieve this. - -Below is the Doppler Secret used in the Getting Started example. We've added a new field to the spec called `processors`. - -```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 - # An object mapping secret names to processor objects - processors: - MY_SECRET: - type: plain -``` - -We currently have one processor for the `MY_SECRET` secret. During parsing, if there is a secret in your Doppler config called `MY_SECRET`, it will be processed using the `plain` parser before it's saved into your managed Kubernetes secret. If you do not specify processors (or you don't specify a processor for a secret in your config), the `plain` processor will be used by default. - -You can have any number of processor: - -```yaml -processors: - MY_SECRET: - type: plain - OTHER_SECRET: - type: plain -``` - -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.