From 9e5b368e6e2c650a4e58d15fe93b4e42e86cc5c8 Mon Sep 17 00:00:00 2001 From: RenkuBot <53332360+RenkuBot@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:08:42 +0200 Subject: [PATCH] release 0.54.0 (#3667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: create release 0.54.0 * chore: Update pureconfig from 0.17.6 to 0.17.7 (#3666) Co-authored-by: RenkuBot * chore: Update circe from 0.14.7 to 0.14.8 (#3683) Co-authored-by: Eike Kettner * chore: Update scalafmt from 3.8.1 to 3.8.2 (#3681) Co-authored-by: Eike Kettner * chore(deps): bump the gh-actions group with 2 updates (#3685) Bumps the gh-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [Azure/docker-login](https://github.com/azure/docker-login). Updates `actions/checkout` from 4.1.2 to 4.1.7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.2...v4.1.7) Updates `Azure/docker-login` from 1 to 2 - [Release notes](https://github.com/azure/docker-login/releases) - [Commits](https://github.com/azure/docker-login/compare/v1...v2) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gh-actions - dependency-name: Azure/docker-login dependency-type: direct:production update-type: version-update:semver-major dependency-group: gh-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ralf Grubenmann * feat: support for rotating secrets storage keys (#3653) * fix: properly encode generated secret service key (#3695) * feat: test storage credentials and prompt on v2 session start (#3693) Co-authored-by: Chandrasekhar Ramakrishnan * Update CHANGELOG.rst Co-authored-by: Flora Thiebaut * chore: Update CHANGELOG.rst wording * chore: Update scalatest from 3.2.18 to 3.2.19 (#3691) Co-authored-by: RenkuBot Co-authored-by: Ralf Grubenmann Co-authored-by: Rok Roškar --------- Signed-off-by: dependabot[bot] Co-authored-by: RenkuBot Co-authored-by: Eike Kettner Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ralf Grubenmann Co-authored-by: Lorenzo Cavazzi <43481553+lorenzo-cavazzi@users.noreply.github.com> Co-authored-by: Chandrasekhar Ramakrishnan Co-authored-by: Flora Thiebaut Co-authored-by: Rok Roškar --- .../workflows/check-acceptance-test-code.yml | 2 +- .../workflows/check-acceptance-test-fmt.yml | 2 +- .github/workflows/create-release-branch.yml | 2 +- .github/workflows/generate-values-script.yaml | 6 +- .github/workflows/publish-helm-chart.yml | 2 +- .github/workflows/publish-master-merges.yaml | 2 +- .github/workflows/pull-request-test.yml | 6 +- .github/workflows/renku-dev-test.yaml | 2 +- CHANGELOG.rst | 27 +++ acceptance-tests/.scalafmt.conf | 2 +- acceptance-tests/build.sbt | 6 +- cypress-tests/cypress/e2e/useSession.cy.ts | 5 +- .../templates/secrets-storage/deployment.yaml | 6 +- .../templates/setup-job-platform-init.yaml | 1 + helm-chart/renku/values.yaml | 30 +-- helm-chart/values.yaml.changelog.md | 19 ++ scripts/platform-init/platform-init.py | 200 ++++++++++++------ 17 files changed, 219 insertions(+), 101 deletions(-) diff --git a/.github/workflows/check-acceptance-test-code.yml b/.github/workflows/check-acceptance-test-code.yml index 8191b650d..a09c3f317 100644 --- a/.github/workflows/check-acceptance-test-code.yml +++ b/.github/workflows/check-acceptance-test-code.yml @@ -8,7 +8,7 @@ jobs: name: Scala dependencies and code check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.7 - name: run test compile run: | cd acceptance-tests diff --git a/.github/workflows/check-acceptance-test-fmt.yml b/.github/workflows/check-acceptance-test-fmt.yml index 7aee0358b..8c6e01e6b 100644 --- a/.github/workflows/check-acceptance-test-fmt.yml +++ b/.github/workflows/check-acceptance-test-fmt.yml @@ -8,7 +8,7 @@ jobs: name: Scala formatting check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.7 - name: run scalafmt run: | cd acceptance-tests diff --git a/.github/workflows/create-release-branch.yml b/.github/workflows/create-release-branch.yml index 83804c3d5..d333c6782 100644 --- a/.github/workflows/create-release-branch.yml +++ b/.github/workflows/create-release-branch.yml @@ -16,7 +16,7 @@ jobs: create-release-pr: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.7 with: fetch-depth: 0 token: "${{ secrets.RENKUBOT_GITHUB_TOKEN }}" diff --git a/.github/workflows/generate-values-script.yaml b/.github/workflows/generate-values-script.yaml index 117755d8e..160f78f84 100644 --- a/.github/workflows/generate-values-script.yaml +++ b/.github/workflows/generate-values-script.yaml @@ -15,7 +15,7 @@ jobs: - os: macos-11 - os: ubuntu-20.04 steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.7 with: fetch-depth: 0 - uses: actions/setup-python@v5 @@ -49,10 +49,10 @@ jobs: runs-on: ubuntu-20.04 needs: [test-script] steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.7 with: fetch-depth: 0 - - uses: Azure/docker-login@v1 + - uses: Azure/docker-login@v2 with: username: ${{ secrets.RENKU_DOCKER_USERNAME }} password: ${{ secrets.RENKU_DOCKER_PASSWORD }} diff --git a/.github/workflows/publish-helm-chart.yml b/.github/workflows/publish-helm-chart.yml index 25608c07b..30422141e 100644 --- a/.github/workflows/publish-helm-chart.yml +++ b/.github/workflows/publish-helm-chart.yml @@ -9,7 +9,7 @@ jobs: publish-chart: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.7 with: fetch-depth: 0 - name: Set version diff --git a/.github/workflows/publish-master-merges.yaml b/.github/workflows/publish-master-merges.yaml index cf8a596a6..df49c7c86 100644 --- a/.github/workflows/publish-master-merges.yaml +++ b/.github/workflows/publish-master-merges.yaml @@ -14,7 +14,7 @@ jobs: publish-chart: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.7 with: fetch-depth: 0 - uses: azure/setup-helm@v4 diff --git a/.github/workflows/pull-request-test.yml b/.github/workflows/pull-request-test.yml index 1ca3b079a..5e5030fbf 100644 --- a/.github/workflows/pull-request-test.yml +++ b/.github/workflows/pull-request-test.yml @@ -18,7 +18,7 @@ jobs: if: github.event.action != 'closed' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.7 - uses: actions/setup-java@v4 with: distribution: "temurin" @@ -60,7 +60,7 @@ jobs: test-enabled: ${{ steps.deploy-comment.outputs.test-enabled}} extra-values: ${{ steps.deploy-comment.outputs.extra-values}} steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.7 - id: deploy-comment uses: SwissDataScienceCenter/renku-actions/check-pr-description@v1.11.3 with: @@ -75,7 +75,7 @@ jobs: name: ci-renku-${{ github.event.number }} url: https://ci-renku-${{ github.event.number }}.dev.renku.ch steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.7 - name: renku build and deploy if: needs.check-deploy.outputs.pr-contains-string == 'true' uses: SwissDataScienceCenter/renku-actions/deploy-renku@v1.11.3 diff --git a/.github/workflows/renku-dev-test.yaml b/.github/workflows/renku-dev-test.yaml index 3fdea1424..60074e294 100644 --- a/.github/workflows/renku-dev-test.yaml +++ b/.github/workflows/renku-dev-test.yaml @@ -22,7 +22,7 @@ jobs: github.event.client_payload.message == 'Helm test succeeded' }} runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.7 - uses: cypress-io/github-action@v6 id: cypress env: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1ed753c97..f7d693546 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,32 @@ .. _changelog: +0.54.0 +------ + +User-Facing Changes +~~~~~~~~~~~~~~~~~~~ + +**✨ Improvements** + +- Test the cloud storage connection before persisting the configuration (`#3194 `_) +- Prompt for cloud storage credentials on v2 session start (`#3203 `_) +- Indicate repository permissions in Renku 2.0 (`#3136 `_) + +Internal Changes +~~~~~~~~~~~~~~~~ + +**🌟 New Features** + +- **Secrets**: Allow rotating the private key for secrets storage + +Individual Components +~~~~~~~~~~~~~~~~~~~~~ + +- `renku-data-services 0.15.0 `__ +- `renku-notebooks 1.25.2 `_ +- `renku-ui 3.29.0 `_ + + 0.53.1 ------ diff --git a/acceptance-tests/.scalafmt.conf b/acceptance-tests/.scalafmt.conf index 3e189db71..0b11aa9fb 100644 --- a/acceptance-tests/.scalafmt.conf +++ b/acceptance-tests/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.8.1" +version = "3.8.2" runner.dialect = "scala213" diff --git a/acceptance-tests/build.sbt b/acceptance-tests/build.sbt index 20dd313b6..d2107dbcc 100644 --- a/acceptance-tests/build.sbt +++ b/acceptance-tests/build.sbt @@ -28,10 +28,10 @@ enablePlugins(AutomateHeaderPlugin) publish / skip := true publishTo := Some(Resolver.file("Unused transient repository", file("target/unusedrepo"))) -val circeVersion = "0.14.7" +val circeVersion = "0.14.8" libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.5.6" -libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.17.6" % Test +libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.17.7" % Test libraryDependencies += "eu.timepit" %% "refined" % "0.11.2" % Test libraryDependencies += "io.circe" %% "circe-core" % circeVersion % Test libraryDependencies += "io.circe" %% "circe-literal" % circeVersion % Test @@ -40,7 +40,7 @@ libraryDependencies += "io.circe" %% "circe-optics" libraryDependencies += "org.http4s" %% "http4s-blaze-client" % "0.23.16" % Test libraryDependencies += "org.http4s" %% "http4s-circe" % "0.23.27" % Test libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.18.0" % Test -libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.18" % Test +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % Test libraryDependencies += "org.scalatestplus" %% "selenium-4-1" % "3.2.12.1" % Test libraryDependencies += "org.seleniumhq.selenium" % "selenium-http-jdk-client" % "4.13.0" % Test libraryDependencies += "org.seleniumhq.selenium" % "selenium-java" % "4.18.1" % Test diff --git a/cypress-tests/cypress/e2e/useSession.cy.ts b/cypress-tests/cypress/e2e/useSession.cy.ts index ed665cd3d..79b989c6e 100644 --- a/cypress-tests/cypress/e2e/useSession.cy.ts +++ b/cypress-tests/cypress/e2e/useSession.cy.ts @@ -265,7 +265,8 @@ describe("Basic public project functionality", () => { cy.get("#endpoint") .should("have.value", "") .type("http://s3.amazonaws.com"); - cy.getDataCy("cloud-storage-edit-next-button") + cy.getDataCy("test-cloud-storage-button").should("be.visible").click(); + cy.getDataCy("add-cloud-storage-continue-button") .should("be.visible") .click(); @@ -282,7 +283,7 @@ describe("Basic public project functionality", () => { .click(); cy.getDataCy("cloud-storage-edit-body").contains( - "storage data_s3 has been succesfully added" + "storage data_s3 has been successfully added" ); cy.getDataCy("cloud-storage-edit-close-button") .should("be.visible") diff --git a/helm-chart/renku/templates/secrets-storage/deployment.yaml b/helm-chart/renku/templates/secrets-storage/deployment.yaml index 489fda6f0..13e2d972b 100644 --- a/helm-chart/renku/templates/secrets-storage/deployment.yaml +++ b/helm-chart/renku/templates/secrets-storage/deployment.yaml @@ -24,6 +24,7 @@ spec: release: {{ .Release.Name }} {{- with .Values.secretsStorage.podAnnotations }} annotations: + checksum/privateKey: {{ .Values.global.platformConfig | sha256sum }} {{- toYaml . | nindent 8 }} {{- end }} spec: @@ -67,6 +68,8 @@ spec: key: dataServiceKeycloakClientSecret - name: SECRETS_SERVICE_PRIVATE_KEY_PATH value: /secrets/privateKey/privateKey + - name: PREVIOUS_SECRETS_SERVICE_PRIVATE_KEY_PATH + value: /secrets/privateKey/previousPrivateKey {{- include "certificates.env.python" $ | nindent 12 }} livenessProbe: httpGet: @@ -99,9 +102,6 @@ spec: - 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: diff --git a/helm-chart/renku/templates/setup-job-platform-init.yaml b/helm-chart/renku/templates/setup-job-platform-init.yaml index 6d1cf05a9..053a4d315 100644 --- a/helm-chart/renku/templates/setup-job-platform-init.yaml +++ b/helm-chart/renku/templates/setup-job-platform-init.yaml @@ -60,6 +60,7 @@ rules: - list - patch - create + - delete --- apiVersion: v1 kind: ServiceAccount diff --git a/helm-chart/renku/values.yaml b/helm-chart/renku/values.yaml index 2b33dbb33..b860c13a9 100644 --- a/helm-chart/renku/values.yaml +++ b/helm-chart/renku/values.yaml @@ -7,8 +7,8 @@ global: ## YAML string that contains all application level Renku configuration options. platformConfig: | - {} - # secretServicePrivateKey: ... RSA Private Key in PEM format + # secretServicePrivateKey: ... RSA Private Key in PKCS8 PEM format (`ssh-keygen -m PKCS8 -t rsa -b 4096`) + # secretServicePreviousPrivateKey: ... Previous Private key in PEM format, only set this when rotating keys # dataServiceEncryptionKey: 32 byte random string gitlab: ## Name of the postgres database to be used by Gitlab @@ -672,7 +672,7 @@ ui: replicaCount: 1 image: repository: renku/renku-ui - tag: "3.28.1" + tag: "3.29.0" pullPolicy: IfNotPresent ## Optionally specify an array of imagePullSecrets. ## Secrets must be manually created in the namespace. @@ -861,7 +861,7 @@ ui: keepCookies: [] image: repository: renku/renku-ui-server - tag: "3.28.1" + tag: "3.29.0" pullPolicy: IfNotPresent imagePullSecrets: [] nameOverride: "" @@ -1001,7 +1001,7 @@ notebooks: targetCPUUtilizationPercentage: 50 image: repository: renku/renku-notebooks - tag: "1.25.1" + tag: "1.25.2" pullPolicy: IfNotPresent ## Optionally specify an array of imagePullSecrets. ## Secrets must be manually created in the namespace. @@ -1119,15 +1119,15 @@ notebooks: gitRpcServer: image: name: renku/git-rpc-server - tag: "1.25.1" + tag: "1.25.2" gitHttpsProxy: image: name: renku/git-https-proxy - tag: "1.25.1" + tag: "1.25.2" gitClone: image: name: renku/git-clone - tag: "1.25.1" + tag: "1.25.2" service: type: ClusterIP port: 80 @@ -1180,12 +1180,12 @@ notebooks: sessionTypes: ["registered"] image: repository: renku/renku-notebooks-tests - tag: "1.25.1" + tag: "1.25.2" pullPolicy: IfNotPresent k8sWatcher: image: repository: renku/k8s-watcher - tag: "1.25.1" + tag: "1.25.2" pullPolicy: IfNotPresent resources: {} replicaCount: 1 @@ -1197,12 +1197,12 @@ notebooks: secretsMount: image: repository: renku/secrets-mount - tag: "1.25.1" + tag: "1.25.2" ssh: enabled: false image: repository: renku/ssh-jump-host - tag: "1.25.1" + tag: "1.25.2" pullPolicy: IfNotPresent resources: {} replicaCount: 1 @@ -1603,14 +1603,14 @@ platformInit: dataService: image: repository: renku/renku-data-service - tag: "0.14.1" + tag: "0.15.0" pullPolicy: IfNotPresent backgroundJobs: events: resources: {} image: repository: renku/data-service-background-jobs - tag: "0.14.1" + tag: "0.15.0" pullPolicy: IfNotPresent total: resources: {} @@ -1663,7 +1663,7 @@ authz: secretsStorage: image: repository: renku/secrets-storage - tag: "0.14.1" + tag: "0.15.0" pullPolicy: IfNotPresent service: type: ClusterIP diff --git a/helm-chart/values.yaml.changelog.md b/helm-chart/values.yaml.changelog.md index ef67791c6..df3e0a854 100644 --- a/helm-chart/values.yaml.changelog.md +++ b/helm-chart/values.yaml.changelog.md @@ -5,6 +5,17 @@ For changes that require manual steps other than changing values, please check o Please follow this convention when adding a new row * ` - **:
` +## Upgrading to Renku 0.54.0 + +* NEW ``global.platformConfig``: The YAML string can now contain a new key, `secretServicePreviousPrivateKey` which allows for rotating the secret-storage private key. + To rotate keys, set this to the previous `secretServicePrivateKey` value and set a new key for `secretServicePrivateKey`. Secrets-storage will then rotate all secrets + once its started. You can monitor the progress of the rotation in prometheus using the `secrets_rotation_count` (for total secrets rotated so far) and `secrets_rotation_state` + (either `running`, `finished` or `errored`) for the overall state of the rotation. Please make sure to unset `secretServicePreviousPrivateKey` once rotation is finished + as a matter of best practice. + + NOTE: Make sure that you do not redeploy or rollback the Renku Helm chart while a key rotation is underway. Even if the + deployment is broken it is best to wait for the key rotation to finish before attempting another deployment or a rollback. + ## Upgrading to Renku 0.53.0 The `data-service` configuration has been updated to support trusting reverse proxies. @@ -18,6 +29,14 @@ The `data-service` configuration has been updated to support trusting reverse pr * NEW ``dataService.backgroundJobs.events.resources`` to set the resources for the users short period synchronization job * NEW ``dataService.backgroundJobs.total.resources`` to set the resources for the users long period synchronization job +## Upgrading to Renku 0.52.0 + +* NEW ``global.platformConfig`` a YAML string that contains the secret keys used by renku-data-services and secrets storage. In the future we plan to also consolidate other + platform specific configuration here. The YAML string should contain the following keys: + - `secretServicePrivateKey`: An RSA private key, generated by `ssh-keygen -m PKCS8 -t rsa -b 4096` without a password. You can leave this empty to have one automatically generated + but we recommend setting it manually. + - `dataServiceEncryptionKey`: A 32 byte random string, used for encryption at rest. + ## Upgrading to Renku 0.51.0 * NEW ``ui.client.sessionClassEmailUs`` to customize the content of the Email Us button on the Session class option. diff --git a/scripts/platform-init/platform-init.py b/scripts/platform-init/platform-init.py index 128958716..bfe2fecf6 100644 --- a/scripts/platform-init/platform-init.py +++ b/scripts/platform-init/platform-init.py @@ -9,24 +9,37 @@ from cryptography.hazmat.primitives import serialization from cryptography.fernet import Fernet +PRIVATE_KEY_ENTRY_NAME = "privateKey" +PUBLIC_KEY_ENTRY_NAME = "publicKey" +PREVIOUS_PRIVATE_KEY_ENTRY_NAME = "previousPrivateKey" + @dataclass class Config: k8s_namespace: str renku_fullname: str + secret_service_private_key_secret_name: str + secret_service_public_key_secret_name: str secret_service_private_key: str | None = field(repr=False) encryption_key: str | None = field(repr=False) + previous_secret_service_private_key: str | None = field(repr=False) @classmethod def from_env(cls): config_map = yaml.load( os.environ.get("PLATFORM_INIT_CONFIG", "{}"), Loader=yaml.Loader ) + renku_fullname = os.environ["RENKU_FULLNAME"] return cls( k8s_namespace=os.environ["K8S_NAMESPACE"], - renku_fullname=os.environ["RENKU_FULLNAME"], + renku_fullname=renku_fullname, secret_service_private_key=config_map.get("secretServicePrivateKey"), encryption_key=config_map.get("dataServiceEncryptionKey"), + previous_secret_service_private_key=config_map.get( + "secretServicePreviousPrivateKey" + ), + secret_service_private_key_secret_name=f"{renku_fullname}-secret-service-private-key", + secret_service_public_key_secret_name=f"{renku_fullname}-secret-service-public-key", ) @@ -48,99 +61,156 @@ def _get_k8s_secret(namespace: str, secret_name: str, secret_key: str) -> str | return None -def init_secret_service_secret(config: Config): - """Initialize private and public key for secrets storage service.""" - logging.info("Initializing secret service secret") +def set_public_key(config: Config): + """Set the secrets storage public key.""" 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 + config.k8s_namespace, + config.secret_service_public_key_secret_name, + 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(), - ) + private_key_rsa = serialization.load_pem_private_key( + config.secret_service_private_key.encode(), password=None + ) + public_key_pem = private_key_rsa.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={private_key_entry_name: b64encode(private_key_pem).decode()}, + data={PUBLIC_KEY_ENTRY_NAME: b64encode(public_key_pem).decode()}, kind="Secret", metadata={ - "name": private_key_secret, + "name": config.secret_service_public_key_secret_name, "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( + else: + v1.patch_namespaced_secret( + config.secret_service_public_key_secret_name, config.k8s_namespace, k8s_client.V1Secret( api_version="v1", - data={ - private_key_entry_name: b64encode( - config.secret_service_private_key.encode() - ).decode() - }, + data={PUBLIC_KEY_ENTRY_NAME: b64encode(public_key_pem).decode()}, kind="Secret", metadata={ - "name": private_key_secret, + "name": config.secret_service_public_key_secret_name, "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 + + +def setup_private_key_rotation(config: Config, existing_key: str | None): + """Setup secrets storage secret for private key rotation. + + Note: In theory, wy don't need a config value for the previous secrets at all, we could just rotate secrets if the secret in config + doesn't match the one in kubernetes. But since secrets rotation is an expensive and destructive operation, we require it set in config + and do some sanity checking here. + """ + if existing_key is None: + raise ValueError( + "Cannot set up secrets key rotation without existing private key" + ) + if existing_key == config.secret_service_private_key: + raise ValueError( + "Can't set up secrets key rotation, new key matches existing k8s key" + ) + if config.previous_secret_service_private_key != existing_key: + raise ValueError( + "secretServicePreviousPrivateKey does not match existing k8s secrets key, did you misconfigure secrets rotation?" ) - # generate public key - public_key_pem = private_key.public_key().public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, + v1 = k8s_client.CoreV1Api() + + v1.patch_namespaced_secret( + config.secret_service_private_key_secret_name, + config.k8s_namespace, + k8s_client.V1Secret( + api_version="v1", + data={ + PRIVATE_KEY_ENTRY_NAME: b64encode( + config.secret_service_private_key.encode() + ).decode(), + PREVIOUS_PRIVATE_KEY_ENTRY_NAME: b64encode( + config.previous_secret_service_private_key.encode() + ).decode(), + }, + kind="Secret", + metadata={ + "name": config.secret_service_private_key_secret_name, + "namespace": config.k8s_namespace, + }, + type="Opaque", + ), ) - 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", - ), + + set_public_key(config) + + +def initialize_private_key(config: Config): + """Set the secret service private key.""" + + if not config.secret_service_private_key: + # autogenerate a key. This is mostly for convenience in CI deployments. + private_key_rsa = rsa.generate_private_key(public_exponent=65537, key_size=4096) + private_key_pem = private_key_rsa.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), ) - 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", - ), + config.secret_service_private_key = private_key_pem.decode() + + v1 = k8s_client.CoreV1Api() + + 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": config.secret_service_private_key_secret_name, + "namespace": config.k8s_namespace, + }, + type="Opaque", + ), + ) + set_public_key(config) + + +def set_secret_service_secrets(config: Config): + """Initialize private and public key for secrets storage service.""" + logging.info("Initializing secret service secret") + + private_key_secret = f"{config.renku_fullname}-secret-service-private-key" + existing_private_key = _get_k8s_secret( + config.k8s_namespace, private_key_secret, PRIVATE_KEY_ENTRY_NAME + ) + + if config.previous_secret_service_private_key: + setup_private_key_rotation(config, existing_private_key) + elif not existing_private_key: + initialize_private_key(config) + elif ( + existing_private_key is not None + and config.secret_service_private_key is not None + and existing_private_key != config.secret_service_private_key + ): + raise ValueError( + "Private key in config does not match private key in k8s and not performing key rotation" ) @@ -193,7 +263,7 @@ def main(): k8s_config.load_incluster_config() logging.basicConfig(level=logging.INFO) logging.info("Initializing Renku platform") - init_secret_service_secret(config) + set_secret_service_secrets(config) init_secret_and_data_service_encryption(config)