diff --git a/deploy/helm/secret-operator/templates/secret_migration_job.yaml b/deploy/helm/secret-operator/templates/secret_migration_job.yaml new file mode 100644 index 00000000..525c7613 --- /dev/null +++ b/deploy/helm/secret-operator/templates/secret_migration_job.yaml @@ -0,0 +1,55 @@ +--- +# Migrates the TLS CA keypair from the hard-coded default namespace to the operator namespace +# See https://github.com/stackabletech/secret-operator/issues/453 +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "operator.fullname" . }}-secret-migration + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-delete-policy": hook-succeeded + "helm.sh/hook-weight": "-5" + labels: + {{- include "operator.labels" . | nindent 4 }} +spec: + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "operator.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.image.pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "operator.fullname" . }}-secret-migration-serviceaccount + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: migrate-secret + image: "{{ .Values.secretMigrationJob.image.repository }}:1.0.0-stackable{{ .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.secretMigrationJob.image.pullPolicy }} + resources: + {{ .Values.secretMigrationJob.resources | toYaml | nindent 12 }} + command: ["bash", "-c"] + args: + - | + #!/bin/bash + set -euo pipefail + SOURCE_NAMESPACE=default + TARGET_NAMESPACE={{ .Values.secretClasses.tls.caSecretNamespace | default .Release.Namespace }} + + # only continue if secret exists + if source_ca_secret="$(kubectl get secret -n $SOURCE_NAMESPACE secret-provisioner-tls-ca -o json)"; then + echo "secret exists in namespace $SOURCE_NAMESPACE" + # only continue if secret in target namespace does NOT exist + if ! kubectl get secret -n $TARGET_NAMESPACE secret-provisioner-tls-ca; then + echo "secret does not exist in namespace $TARGET_NAMESPACE" + # copy secret from default to {{ .Values.secretClasses.tls.caSecretNamespace | default .Release.Namespace }} + echo "$source_ca_secret" | jq 'del(.metadata["namespace","creationTimestamp","resourceVersion","selfLink","uid"])' | kubectl apply -n $TARGET_NAMESPACE -f - + fi + fi + restartPolicy: Never \ No newline at end of file diff --git a/deploy/helm/secret-operator/templates/secret_migration_rbac.yaml b/deploy/helm/secret-operator/templates/secret_migration_rbac.yaml new file mode 100644 index 00000000..d4a462d1 --- /dev/null +++ b/deploy/helm/secret-operator/templates/secret_migration_rbac.yaml @@ -0,0 +1,56 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "operator.fullname" . }}-secret-migration-serviceaccount + labels: + {{- include "operator.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-delete-policy": hook-succeeded + "helm.sh/hook-weight": "-10" + {{- with .Values.serviceAccount.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "operator.fullname" . }}-secret-migration-clusterrolebinding + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-delete-policy": hook-succeeded + "helm.sh/hook-weight": "-10" + labels: + {{- include "operator.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + name: {{ include "operator.fullname" . }}-secret-migration-serviceaccount + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: {{ include "operator.fullname" . }}-secret-migration-clusterrole + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "operator.fullname" . }}-secret-migration-clusterrole + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-delete-policy": hook-succeeded + "helm.sh/hook-weight": "-10" + labels: + {{- include "operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - create + - patch + - update \ No newline at end of file diff --git a/deploy/helm/secret-operator/values.yaml b/deploy/helm/secret-operator/values.yaml index f12f7cc1..452b69ff 100644 --- a/deploy/helm/secret-operator/values.yaml +++ b/deploy/helm/secret-operator/values.yaml @@ -5,6 +5,18 @@ image: pullPolicy: IfNotPresent pullSecrets: [] +secretMigrationJob: + image: + repository: docker.stackable.tech/stackable/tools + pullPolicy: IfNotPresent + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 100m + memory: 128Mi + csiProvisioner: image: repository: docker.stackable.tech/k8s/sig-storage/csi-provisioner diff --git a/docs/modules/secret-operator/examples/cert-manager/certificate.yaml b/docs/modules/secret-operator/examples/cert-manager/certificate.yaml new file mode 100644 index 00000000..0e838091 --- /dev/null +++ b/docs/modules/secret-operator/examples/cert-manager/certificate.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: my-app-tls # <1> +spec: + secretName: my-app-tls # <2> + secretTemplate: + labels: + secrets.stackable.tech/class: tls-cert-manager # <3> + secrets.stackable.tech/service: my-app # <4> + dnsNames: + - my-app # <5> + issuerRef: + kind: Issuer + name: secret-operator-demonstration # <6> diff --git a/docs/modules/secret-operator/examples/cert-manager/issuer.yaml b/docs/modules/secret-operator/examples/cert-manager/issuer.yaml new file mode 100644 index 00000000..67e8fa14 --- /dev/null +++ b/docs/modules/secret-operator/examples/cert-manager/issuer.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: secret-operator-demonstration # <1> +spec: + ca: + secretName: secret-operator-demonstration-ca +# Create a self-signed CA for secret-operator-demonstration to use +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: secret-operator-demonstration-ca +spec: + secretName: secret-operator-demonstration-ca + isCA: true + commonName: Stackable Secret Operator/Cert-Manager Demonstration CA + issuerRef: + kind: Issuer + name: secret-operator-demonstration-ca +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: secret-operator-demonstration-ca +spec: + selfSigned: {} diff --git a/docs/modules/secret-operator/examples/cert-manager/pod.yaml b/docs/modules/secret-operator/examples/cert-manager/pod.yaml new file mode 100644 index 00000000..3011b752 --- /dev/null +++ b/docs/modules/secret-operator/examples/cert-manager/pod.yaml @@ -0,0 +1,71 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + replicas: 1 + selector: + matchLabels: + app: my-app + template: + metadata: + labels: + app: my-app + spec: + containers: + - name: nginx + image: nginx + volumeMounts: + - name: tls + mountPath: /tls + - name: config + mountPath: /etc/nginx/conf.d + ports: + - name: https + containerPort: 443 + volumes: + - name: tls # <1> + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls-cert-manager # <2> + secrets.stackable.tech/scope: service=my-app # <3> + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + - name: config + configMap: + name: my-app +--- # <4> +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-app +data: + default.conf: | + server { + listen 443 ssl; + ssl_certificate /tls/tls.crt; + ssl_certificate_key /tls/tls.key; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + } +--- # <5> +apiVersion: v1 +kind: Service +metadata: + name: my-app +spec: + selector: + app: my-app + ports: + - name: https + port: 443 diff --git a/docs/modules/secret-operator/examples/cert-manager/secretclass.yaml b/docs/modules/secret-operator/examples/cert-manager/secretclass.yaml new file mode 100644 index 00000000..cb8ef427 --- /dev/null +++ b/docs/modules/secret-operator/examples/cert-manager/secretclass.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: SecretClass +metadata: + name: tls-cert-manager # <1> +spec: + backend: + k8sSearch: + searchNamespace: + pod: {} # <2> diff --git a/docs/modules/secret-operator/pages/cert-manager.adoc b/docs/modules/secret-operator/pages/cert-manager.adoc new file mode 100644 index 00000000..a03d7ae6 --- /dev/null +++ b/docs/modules/secret-operator/pages/cert-manager.adoc @@ -0,0 +1,82 @@ += Cert-Manager Integration + +https://cert-manager.io/[Cert-Manager] is a common tool to manage certificates in Kubernetes, especially when backed by an external +Certificate Authority (CA) such as https://letsencrypt.org/[Let\'s Encrypt]. + +The Stackable Secret Operator does not currently support managing Cert-Manager certificates directly, but it can be configured to consume certificates generated by it. + +[#caveats] +== Caveats + +Cert-Manager is designed to manage relatively long-lived certificates that are stored in Kubernetes Secrets. By contrast, +the Stackable Secret Operator is designed to generate temporary short-lived certificates. + +This has a couple of repercussions: + +- Longer-lived certificates mean that a leaked certificate has potential to be abused for longer. +- Application teams may have access to read Secrets in their respective applications' Namespaces. + +Where possible, we recommend using the xref:secretclass.adoc#backend-autotls[`autoTls` backend] instead. + +[#issuer] +== Configuring Cert-Manager + +NOTE: We recommend using the xref:secretclass.adoc#backend-autotls[`autoTls` backend] instead for self-signed PKIs. We use Cert-Manager's CA issuer here to show the broader concepts. + +To do this, you will first need to teach Cert-Manager how to create your certificates. + +In a production setup this will likely use an external CA such as ACME or OpenBao/Vault. However, to make this guide self-contained, Cert-Manager will create +a self-signed CA certificate instead. + +[source,yaml] +---- +include::example$cert-manager/issuer.yaml[] +---- +<1> This is the Issuer that our created certificates will reference later + +[#secretclass] +== Creating a SecretClass + +The Stackable Secret Operator needs to know how to find the certificates created by Cert-Manager. We do this by creating +a xref:secretclass.adoc[] using the xref:secretclass.adoc#backend-k8ssearch[`k8sSearch` backend], which can find arbitrary +Kubernetes Secret objects that have the correct labels. + +[source,yaml] +---- +include::example$cert-manager/secretclass.yaml[] +---- +<1> Both certificates and Pods will reference this name, to ensure that the correct certificates are found +<2> This informs the Secret Operator that certificates will be found in the same namespace as the Pod using it + +[#certificate] +== Requesting a certificate + +You can now use Cert-Manager to provision your first certificate. Use labels to inform the Stackable Secret Operator +about which xref:scope.adoc[scopes] the certificate fulfills. Which scopes must be provisioned is going to depend +on the design of the workload. This guide assumes the xref:scope.adoc#service[service] scope. + +[source,yaml] +---- +include::example$cert-manager/certificate.yaml[] +---- +<1> The Certificate name is irrelevant for the Stackable Secret Operator's, but must be unique (within the Namespace) +<2> The Secret name must also be unique within the Namespace +<3> This tells the Stackable Secret Operator that this secret corresponds to the SecretClass created xref:#secretclass[before] +<4> This secret fulfils the xref:scope.adoc#service[service] scope for `my-app` +<5> The list of DNS names that this certificate should apply to. +<6> The Cert-Manager Issuer that should sign this certificate, as created xref:#issuer[before] + +[#pod] +== Using the certificate + +Finally, we can create and expose a Pod that consumes the certificate! + +[source,yaml] +---- +include::example$cert-manager/pod.yaml[] +---- +<1> A secret xref:volume.adoc[volume] is created, where the certificate will be exposed to the app +<2> The volume references the SecretClass defined xref:#secretclass[before] +<3> The app is designated the scope xref:scope#service[`service=my-app`], matching the xref:#certificate[certificate's scope] +<4> nginx is configured to use the mounted certificate +<5> nginx is exposed as a Kubernetes Service diff --git a/docs/modules/secret-operator/partials/nav.adoc b/docs/modules/secret-operator/partials/nav.adoc index b9df9d2a..7ff84ca7 100644 --- a/docs/modules/secret-operator/partials/nav.adoc +++ b/docs/modules/secret-operator/partials/nav.adoc @@ -6,6 +6,8 @@ ** xref:secret-operator:secretclass.adoc[] ** xref:secret-operator:scope.adoc[] ** xref:secret-operator:volume.adoc[] +* Guides +** xref:secret-operator:cert-manager.adoc[] * xref:secret-operator:security.adoc[] * xref:secret-operator:reference/index.adoc[] ** xref:secret-operator:reference/crds.adoc[]