diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 139ec24655..48c09f8a72 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,21 @@ .. _changelog: -0.52.x +0.52.0 ------ +Renku ``0.52.0`` contains a new secrets-storage service that allows users to store and use secrets in +sessions. + + +**🌟 New Features** + +- **Data Services**: Added new secrets storage service for managing user session secrets, including + new endpoints on data-service to manage these secrets. + +Individual Components +~~~~~~~~~~~~~~~~~~~~~ + +- `renku-data-services 0.9.0 `_ 0.51.1 ------ diff --git a/chartpress.yaml b/chartpress.yaml index 1b55b12f21..d99c16ed36 100644 --- a/chartpress.yaml +++ b/chartpress.yaml @@ -20,6 +20,7 @@ charts: - acceptance-tests - scripts/init-realm - scripts/init-db + - scripts/platform-init images: tests: buildArgs: @@ -43,3 +44,9 @@ charts: valuesPath: initDb.image paths: - scripts/init-db + platform-init: + contextPath: scripts/platform-init + dockerfilePath: scripts/platform-init/Dockerfile + valuesPath: platformInit.image + paths: + - scripts/platform-init diff --git a/docs/topic-guides/index.rst b/docs/topic-guides/index.rst index e075d3ec5e..bbf0790ef6 100644 --- a/docs/topic-guides/index.rst +++ b/docs/topic-guides/index.rst @@ -7,6 +7,6 @@ Topic Guides :maxdepth: 2 Sessions - Data + Data Workflows Miscellaneous diff --git a/helm-chart/renku/templates/data-service/deployment.yaml b/helm-chart/renku/templates/data-service/deployment.yaml index 8f9ef11b51..b5d6195d55 100644 --- a/helm-chart/renku/templates/data-service/deployment.yaml +++ b/helm-chart/renku/templates/data-service/deployment.yaml @@ -66,6 +66,10 @@ spec: secretKeyRef: name: {{ include "renku.fullname" . }} key: dataServiceKeycloakClientSecret + - name: ENCRYPTION_KEY_PATH + value: /secrets/encryptionKey/encryptionKey + - name: SECRETS_SERVICE_PUBLIC_KEY_PATH + value: /secrets/publicKey/publicKey - name: SERVER_DEFAULTS value: /etc/renku-data-service/server_options/server_defaults.json - name: SERVER_OPTIONS @@ -103,6 +107,12 @@ spec: volumeMounts: - name: server-options mountPath: /etc/renku-data-service/server_options + - mountPath: "/secrets/encryptionKey" + name: encryption-key + readOnly: true + - mountPath: "/secrets/publicKey" + name: secret-service-public-key + readOnly: true {{- include "certificates.volumeMounts.system" . | nindent 12 }} livenessProbe: httpGet: @@ -142,5 +152,17 @@ spec: - name: server-options configMap: name: {{ template "renku.fullname" . }}-server-options + - name: encryption-key + secret: + secretName: {{ template "renku.fullname" . }}-secrets-storage + items: + - key: encryptionKey + path: encryptionKey + - name: secret-service-public-key + secret: + secretName: {{ template "renku.fullname" . }}-secret-service-public-key + items: + - key: publicKey + path: publicKey {{- include "certificates.volumes" . | nindent 8 }} serviceAccountName: {{ template "renku.fullname" . }}-data-service diff --git a/helm-chart/renku/templates/notebooks/network-policy.yaml b/helm-chart/renku/templates/notebooks/network-policy.yaml index cd074dd8a0..7de4810309 100644 --- a/helm-chart/renku/templates/notebooks/network-policy.yaml +++ b/helm-chart/renku/templates/notebooks/network-policy.yaml @@ -93,3 +93,12 @@ spec: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 + - to: + # Allow access to data service + - podSelector: + matchLabels: + app: renku-data-service + ports: + - port: http + protocol: TCP + diff --git a/helm-chart/renku/templates/notebooks/statefulset.yaml b/helm-chart/renku/templates/notebooks/statefulset.yaml index fca2f84375..f800651ec3 100644 --- a/helm-chart/renku/templates/notebooks/statefulset.yaml +++ b/helm-chart/renku/templates/notebooks/statefulset.yaml @@ -184,6 +184,10 @@ spec: value: {{ .Values.notebooks.dummyStores | quote }} - name: NB_DATA_SERVICE_URL value: {{ printf "http://%s-data-service/api/data" .Release.Name}} + - name: NB_USER_SECRETS__SECRETS_STORAGE_SERVICE_URL + value: {{ printf "http://%s-secrets-storage" .Release.Name}} + - name: NB_USER_SECRETS__IMAGE + value: "{{ .Values.notebooks.secretsMount.image.repository}}:{{.Values.notebooks.secretsMount.image.tag}}" ports: - name: http containerPort: 8000 diff --git a/helm-chart/renku/templates/secrets-storage/deployment.yaml b/helm-chart/renku/templates/secrets-storage/deployment.yaml new file mode 100644 index 0000000000..489fda6f0f --- /dev/null +++ b/helm-chart/renku/templates/secrets-storage/deployment.yaml @@ -0,0 +1,110 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "renku.fullname" . }}-secrets-storage + labels: + app: renku-secrets-storage + chart: {{ template "renku.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + {{- if not .Values.secretsStorage.autoscaling.enabled }} + replicas: {{ .Values.secretsStorage.replicaCount }} + {{- end }} + strategy: + {{- toYaml .Values.secretsStorage.updateStrategy | nindent 4 }} + selector: + matchLabels: + app: renku-secrets-storage + release: {{ .Release.Name }} + template: + metadata: + labels: + app: renku-secrets-storage + release: {{ .Release.Name }} + {{- with .Values.secretsStorage.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + automountServiceAccountToken: {{ .Values.global.debug }} + initContainers: + {{- include "certificates.initContainer" . | nindent 8 }} + containers: + - name: secrets-storage + image: "{{ .Values.secretsStorage.image.repository }}:{{ .Values.secretsStorage.image.tag }}" + imagePullPolicy: {{ .Values.secretsStorage.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + ports: + - name: http + containerPort: 8000 + protocol: TCP + env: + - name: VERSION + value: {{ .Values.secretsStorage.image.tag | quote }} + - name: DB_HOST + value: {{ template "postgresql.fullname" . }} + - name: DB_USER + value: {{ .Values.global.db.common.username }} + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.global.db.common.passwordSecretName }} + key: password + - name: KEYCLOAK_URL + value: {{ (printf "%s://%s/auth/" (include "renku.http" .) .Values.global.renku.domain) | quote }} + - name: KEYCLOAK_TOKEN_SIGNATURE_ALGS + value: "RS256" + - name: KEYCLOAK_CLIENT_ID + value: {{ include "renku.dataService.keycloak.clientId" . | quote }} + - name: KEYCLOAK_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "renku.fullname" . }} + key: dataServiceKeycloakClientSecret + - name: SECRETS_SERVICE_PRIVATE_KEY_PATH + value: /secrets/privateKey/privateKey + {{- include "certificates.env.python" $ | nindent 12 }} + livenessProbe: + httpGet: + path: /api/secrets/version + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 6 + readinessProbe: + httpGet: + path: /api/secrets/version + port: http + initialDelaySeconds: 10 + periodSeconds: 2 + failureThreshold: 2 + startupProbe: + httpGet: + path: /api/secrets/version + port: http + periodSeconds: 5 + failureThreshold: 60 + resources: + {{ toYaml .Values.secretsStorage.resources | nindent 12 }} + volumeMounts: + - mountPath: "/secrets/privateKey" + name: secret-service-private-key + readOnly: true + {{- include "certificates.volumeMounts.system" . | nindent 12 }} + volumes: + - name: secret-service-private-key + secret: + secretName: {{ template "renku.fullname" . }}-secret-service-private-key + items: + - key: privateKey + path: privateKey + {{- include "certificates.volumes" . | nindent 8 }} + {{- with .Values.secretsStorage.nodeSelector }} + nodeSelector: + {{ toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ template "renku.fullname" . }}-secrets-storage diff --git a/helm-chart/renku/templates/secrets-storage/hpa.yaml b/helm-chart/renku/templates/secrets-storage/hpa.yaml new file mode 100644 index 0000000000..13bb7b6bcf --- /dev/null +++ b/helm-chart/renku/templates/secrets-storage/hpa.yaml @@ -0,0 +1,39 @@ +{{- if .Values.secretsStorage.autoscaling.enabled }} +{{- if semverCompare ">=1.23.0-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: autoscaling/v2 +{{- else -}} +apiVersion: autoscaling/v2beta2 +{{- end }} +kind: HorizontalPodAutoscaler +metadata: + name: {{ template "renku.fullname" . }}-secrets-storage + labels: + app: renku-secrets-storage + chart: {{ template "renku.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ template "renku.fullname" . }}-secrets-storage + minReplicas: {{ .Values.secretsStorage.autoscaling.minReplicas }} + maxReplicas: {{ .Values.secretsStorage.autoscaling.maxReplicas }} + metrics: + {{- if .Values.secretsStorage.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.secretsStorage.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.secretsStorage.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.secretsStorage.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm-chart/renku/templates/secrets-storage/network-policy.yaml b/helm-chart/renku/templates/secrets-storage/network-policy.yaml new file mode 100644 index 0000000000..dbd2381cb3 --- /dev/null +++ b/helm-chart/renku/templates/secrets-storage/network-policy.yaml @@ -0,0 +1,22 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ template "renku.fullname" . }}-secrets-storage +spec: + podSelector: + matchLabels: + app: {{ template "renku.fullname" . }}-secrets-storage + release: {{ .Release.Name }} + policyTypes: + - Ingress + ingress: + - from: + # Only allow ingress to secrets storage from notebooks + - podSelector: + matchLabels: + app: {{ template "renku.notebooks.name" . }} + release: {{ .Release.Name }} + ports: + - protocol: TCP + port: http + diff --git a/helm-chart/renku/templates/secrets-storage/pdb.yaml b/helm-chart/renku/templates/secrets-storage/pdb.yaml new file mode 100644 index 0000000000..afd4b52f93 --- /dev/null +++ b/helm-chart/renku/templates/secrets-storage/pdb.yaml @@ -0,0 +1,17 @@ +{{- if or (gt (int .Values.secretsStorage.replicaCount) 1) (and .Values.secretsStorage.autoscaling.enabled (gt (int .Values.secretsStorage.autoscaling.minReplicas) 1)) }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ template "renku.fullname" . }}-secrets-storage + labels: + app: renku-secrets-storage + chart: {{ template "renku.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + minAvailable: 50% + selector: + matchLabels: + app: renku-secrets-storage + release: {{ .Release.Name }} +{{- end }} diff --git a/helm-chart/renku/templates/secrets-storage/rbac.yaml b/helm-chart/renku/templates/secrets-storage/rbac.yaml new file mode 100644 index 0000000000..90988fc7c4 --- /dev/null +++ b/helm-chart/renku/templates/secrets-storage/rbac.yaml @@ -0,0 +1,54 @@ +{{- $namespaces := list -}} +{{ if .Values.notebooks.sessionsNamespace }} +{{- $namespaces = list .Release.Namespace .Values.notebooks.sessionsNamespace | uniq -}} +{{ else }} +{{- $namespaces = list .Release.Namespace -}} +{{ end }} +{{ range $namespaces }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ template "renku.fullname" $ }}-secrets-storage + namespace: {{ . }} + labels: + app: {{ template "renku.name" $ }} + chart: {{ template "renku.chart" $ }} + release: {{ $.Release.Name }} + heritage: {{ $.Release.Service }} +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ template "renku.fullname" $ }}-secrets-storage + labels: + app: {{ template "renku.name" $ }} + chart: {{ template "renku.chart" $ }} + release: {{ $.Release.Name }} + heritage: {{ $.Release.Service }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ template "renku.fullname" $ }}-secrets-storage +subjects: + - kind: ServiceAccount + name: {{ template "renku.fullname" $ }}-secrets-storage + namespace: {{ $.Release.Namespace }} +{{ end }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "renku.fullname" . }}-secrets-storage + labels: + app: {{ template "renku.name" . }} + chart: {{ template "renku.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} diff --git a/helm-chart/renku/templates/secrets-storage/service.yaml b/helm-chart/renku/templates/secrets-storage/service.yaml new file mode 100644 index 0000000000..3d655a7689 --- /dev/null +++ b/helm-chart/renku/templates/secrets-storage/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "renku.fullname" . }}-secrets-storage + labels: + app: renku-secrets-storage + chart: {{ template "renku.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.secretsStorage.service.type }} + ports: + - port: {{ .Values.secretsStorage.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app: {{ template "renku.name" . }}-secrets-storage + release: {{ .Release.Name }} diff --git a/helm-chart/renku/templates/setup-job-platform-init.yaml b/helm-chart/renku/templates/setup-job-platform-init.yaml new file mode 100644 index 0000000000..6d1cf05a9a --- /dev/null +++ b/helm-chart/renku/templates/setup-job-platform-init.yaml @@ -0,0 +1,96 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ .Release.Name }}-init-renku-platform-rev{{ .Release.Revision }}-{{ randAlphaNum 5 | lower }}" + labels: + app: platform-init + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + chart: {{ template "renku.chart" . }} + annotations: + "helm.sh/hook": pre-install,pre-upgrade,pre-rollback +spec: + template: + metadata: + name: {{ .Release.Name }}-platform-init + labels: + app: platform-init + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + chart: {{ template "renku.chart" . }} + spec: + restartPolicy: Never + containers: + - name: initialize-platform + image: "{{ .Values.platformInit.image.repository }}:{{ .Values.platformInit.image.tag }}" + args: [ "platform-init.py" ] + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + runAsNonRoot: true + env: + - name: K8S_NAMESPACE + value: {{ .Release.Namespace }} + - name: RENKU_FULLNAME + value: {{ template "renku.fullname" . }} + - name: PLATFORM_INIT_CONFIG + value: {{ .Values.global.platformConfig | default (printf "{}") | quote }} + serviceAccountName: {{ template "renku.fullname" . }}-platform-init +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ template "renku.fullname" . }}-platform-init + labels: + app: {{ template "renku.name" . }} + chart: {{ template "renku.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + annotations: + "helm.sh/hook": pre-install,pre-upgrade,pre-rollback + "helm.sh/hook-weight": "-5" +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - patch + - create +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "renku.fullname" . }}-platform-init + labels: + app: {{ template "renku.name" . }} + chart: {{ template "renku.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + annotations: + "helm.sh/hook": pre-install,pre-upgrade,pre-rollback + "helm.sh/hook-weight": "-5" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ template "renku.fullname" . }}-platform-init + labels: + app: {{ template "renku.name" . }} + chart: {{ template "renku.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + annotations: + "helm.sh/hook": pre-install,pre-upgrade,pre-rollback + "helm.sh/hook-weight": "-5" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ template "renku.fullname" . }}-platform-init +subjects: + - kind: ServiceAccount + name: {{ template "renku.fullname" . }}-platform-init + namespace: {{ .Release.Namespace }} diff --git a/helm-chart/renku/values.yaml b/helm-chart/renku/values.yaml index fd56e1ffd7..a1f1385adb 100644 --- a/helm-chart/renku/values.yaml +++ b/helm-chart/renku/values.yaml @@ -5,6 +5,11 @@ ## Global variables ## Shared values/secrets global: + ## YAML string that contains all application level Renku configuration options. + platformConfig: | + {} + # secretServicePrivateKey: ... RSA Private Key in PEM format + # dataServiceEncryptionKey: 32 byte random string gitlab: ## Name of the postgres database to be used by Gitlab postgresDatabase: gitlabhq_production @@ -1580,6 +1585,11 @@ initDb: image: repository: renku/init-db tag: "latest" +## The image used for general platform initialization +platformInit: + image: + repository: renku/platform-init + tag: "latest" dataService: image: repository: renku/renku-data-service @@ -1628,6 +1638,27 @@ authz: targetCPUUtilizationPercentage: 80 # targetMemoryUtilizationPercentage: 80 +secretsStorage: + image: + repository: renku/secrets-storage + tag: "0.1.0" + pullPolicy: IfNotPresent + service: + type: ClusterIP + port: 80 + replicaCount: 1 + podAnnotations: {} + resources: {} + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 5 + targetMemoryUtilizationPercentage: 75 + targetCPUUtilizationPercentage: 75 + updateStrategy: {} + nodeSelector: {} + tolerations: [] + affinity: {} podSecurityContext: {} securityContext: runAsUser: 1000 diff --git a/scripts/init-db/utils.py b/scripts/init-db/utils.py index e9fdfa675e..10355b8877 100644 --- a/scripts/init-db/utils.py +++ b/scripts/init-db/utils.py @@ -1,19 +1,13 @@ -import base64 import logging import requests -from kubernetes import client, config -from kubernetes.config.config_exception import ConfigException -from kubernetes.config.incluster_config import ( - SERVICE_CERT_FILENAME, - SERVICE_TOKEN_FILENAME, - InClusterConfigLoader, -) + from psycopg2 import connect from tenacity import ( - before_sleep_log, - retry, stop_after_attempt, - stop_after_delay, + before_sleep_log, + retry, + stop_after_attempt, + stop_after_delay, wait_fixed, ) @@ -29,10 +23,16 @@ def get_db_connection(*args, **kwargs): return connect(*args, **kwargs, connect_timeout=10) -@retry(stop=(stop_after_attempt(200) | stop_after_delay(600)), wait=wait_fixed(3), reraise=True) +@retry( + stop=(stop_after_attempt(200) | stop_after_delay(600)), + wait=wait_fixed(3), + reraise=True, +) def gitlab_is_online(url: str) -> int: logging.info("Waiting for gitlab to come online...") res = requests.get(url, timeout=10) if res.status_code != 200: - raise ValueError(f"Gitlab is not available at {url}, status code is {res.status_code}") + raise ValueError( + f"Gitlab is not available at {url}, status code is {res.status_code}" + ) return res.status_code diff --git a/scripts/platform-init/Dockerfile b/scripts/platform-init/Dockerfile new file mode 100644 index 0000000000..27b3a02db0 --- /dev/null +++ b/scripts/platform-init/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.12-slim-bookworm +RUN apt-get update && apt-get install -y \ + tini && \ + rm -rf /var/lib/apt/lists/* +COPY requirements.txt platform-init.py /app/ +WORKDIR /app +RUN pip3 install -r requirements.txt +USER 1000:1000 + +ENTRYPOINT [ "tini", "-g", "--", "python" ] diff --git a/scripts/platform-init/platform-init.py b/scripts/platform-init/platform-init.py new file mode 100644 index 0000000000..0dd96e21b4 --- /dev/null +++ b/scripts/platform-init/platform-init.py @@ -0,0 +1,199 @@ +from base64 import b64decode, b64encode +import yaml +import logging +from typing import cast +from kubernetes import client as k8s_client, config as k8s_config +from dataclasses import dataclass, field +import os +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import serialization +from cryptography.fernet import Fernet + + +@dataclass +class Config: + k8s_namespace: str + renku_fullname: str + secret_service_private_key: str | None = field(repr=False) + encryption_key: str | None = field(repr=False) + + @classmethod + def from_env(cls): + config_map = yaml.load( + os.environ.get("PLATFORM_INIT_CONFIG", "{}"), Loader=yaml.Loader + ) + return cls( + k8s_namespace=os.environ["K8S_NAMESPACE"], + renku_fullname=os.environ["RENKU_FULLNAME"], + secret_service_private_key=config_map.get("secretServicePrivateKey"), + encryption_key=config_map.get("dataServiceEncryptionKey"), + ) + + +def _get_k8s_secret(namespace: str, secret_name: str, secret_key: str) -> str | None: + v1 = k8s_client.CoreV1Api() + try: + secret = cast( + k8s_client.V1Secret, + v1.read_namespaced_secret(secret_name, namespace), + ) + if not secret.data: + return None + secret_data = secret.data.get(secret_key) + if secret_data is not None: + secret_data = b64decode(secret_data).decode() + return secret_data + + except k8s_client.ApiException: + return None + + +def init_secret_service_secret(config: Config): + """Initialize private and public key for secrets storage service.""" + logging.info("Initializing secret service secret") + v1 = k8s_client.CoreV1Api() + + private_key_secret = f"{config.renku_fullname}-secret-service-private-key" + private_key_entry_name = "privateKey" + existing_private_key = _get_k8s_secret( + config.k8s_namespace, private_key_secret, private_key_entry_name + ) + + public_key_secret = f"{config.renku_fullname}-secret-service-public-key" + public_key_entry_name = "publicKey" + existing_public_key = _get_k8s_secret( + config.k8s_namespace, public_key_secret, public_key_entry_name + ) + + if existing_private_key is None and config.secret_service_private_key is None: + # generate new secret + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096) + private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + v1.create_namespaced_secret( + config.k8s_namespace, + k8s_client.V1Secret( + api_version="v1", + data={private_key_entry_name: b64encode(private_key_pem).decode()}, + kind="Secret", + metadata={ + "name": private_key_secret, + "namespace": config.k8s_namespace, + }, + type="Opaque", + ), + ) + elif existing_private_key is None and config.secret_service_private_key is not None: + # create private key from config + private_key = serialization.load_pem_private_key( + config.secret_service_private_key.encode(), password=None + ) + v1.create_namespaced_secret( + config.k8s_namespace, + k8s_client.V1Secret( + api_version="v1", + data={ + private_key_entry_name: b64encode( + config.secret_service_private_key.encode() + ).decode() + }, + kind="Secret", + metadata={ + "name": private_key_secret, + "namespace": config.k8s_namespace, + }, + type="Opaque", + ), + ) + else: + # just load key to create public key from + private_key = serialization.load_pem_private_key( + existing_private_key.encode(), password=None + ) + + # generate public key + public_key_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + if existing_public_key is None: + v1.create_namespaced_secret( + config.k8s_namespace, + k8s_client.V1Secret( + api_version="v1", + data={public_key_entry_name: b64encode(public_key_pem).decode()}, + kind="Secret", + metadata={"name": public_key_secret, "namespace": config.k8s_namespace}, + type="Opaque", + ), + ) + else: + v1.patch_namespaced_secret( + public_key_secret, + config.k8s_namespace, + k8s_client.V1Secret( + api_version="v1", + data={public_key_entry_name: b64encode(public_key_pem).decode()}, + kind="Secret", + metadata={"name": public_key_secret, "namespace": config.k8s_namespace}, + type="Opaque", + ), + ) + + +def init_secret_and_data_service_encryption(config: Config): + """Initialize symmetric encryption key for encryption at rest in data service.""" + logging.info("Initializing data service encryption") + v1 = k8s_client.CoreV1Api() + + encryption_key = f"{config.renku_fullname}-secret-storage" + encryption_key_name = "encryptionKey" + existing_encryption_key = _get_k8s_secret( + config.k8s_namespace, encryption_key, encryption_key_name + ) + + if existing_encryption_key is None and config.encryption_key is None: + # generate a symmetric encryption key + key = Fernet.generate_key() + v1.create_namespaced_secret( + config.k8s_namespace, + k8s_client.V1Secret( + api_version="v1", + data={encryption_key_name: b64encode(key).decode()}, + kind="Secret", + metadata={ + "name": encryption_key, + "namespace": config.k8s_namespace, + }, + type="Opaque", + ), + ) + elif existing_encryption_key is None and config.encryption_key is not None: + key = config.encryption_key.encode() + v1.create_namespaced_secret( + config.k8s_namespace, + k8s_client.V1Secret( + api_version="v1", + data={encryption_key_name: b64encode(key).decode()}, + kind="Secret", + metadata={ + "name": encryption_key, + "namespace": config.k8s_namespace, + }, + type="Opaque", + ), + ) + + +def main(): + config = Config.from_env() + k8s_config.load_incluster_config() + logging.info("Initializing Renku platform") + init_secret_service_secret(config) + + +if __name__ == "__main__": + main() diff --git a/scripts/platform-init/requirements.txt b/scripts/platform-init/requirements.txt new file mode 100644 index 0000000000..ede9653b36 --- /dev/null +++ b/scripts/platform-init/requirements.txt @@ -0,0 +1,3 @@ +kubernetes==29.0.0 +cryptography==42.0.5 +pyyaml==6.0.1