From 2e12a9fdc342ed6b84b219d9b4fe8f5f4fc3da16 Mon Sep 17 00:00:00 2001 From: Stephan Butler Date: Wed, 16 Sep 2026 17:04:58 +0200 Subject: [PATCH] fix: helm chart should render hsm-adapter configs --- .../templates/configMap.backend.yaml | 41 +++++++ .../tests/configMap.backend_test.yaml | 109 +++++++++++++++++ .../tests/deployment.backend_test.yaml | 41 +++++++ helm/testnet-wallet/values.yaml | 111 +++++++++++++++--- 4 files changed, 287 insertions(+), 15 deletions(-) create mode 100644 helm/testnet-wallet/tests/configMap.backend_test.yaml diff --git a/helm/testnet-wallet/templates/configMap.backend.yaml b/helm/testnet-wallet/templates/configMap.backend.yaml index 84f232db0..0c9ffe515 100644 --- a/helm/testnet-wallet/templates/configMap.backend.yaml +++ b/helm/testnet-wallet/templates/configMap.backend.yaml @@ -1,3 +1,44 @@ +{{/* +The override adds the HSM adapter block to the ConfigMap the contentMap builds. + +It is not a contentMap entry. The backend validates ATALLA_HOST and ATALLA_PORT +when HSM_ENABLED is true and rejects an empty string for either, so a contentMap +entry would emit `ATALLA_HOST: ""` in every environment that leaves it unset and +stop the backend from starting. Here the whole block appears only when +config.backend.hsm.enabled is true. + +common.utils.merge merges maps deeply, so these keys join the contentMap's +`data` rather than replacing it. +*/}} {{- define "testnet-wallet.backend.ConfigMap" -}} +{{- $top := first . }} +{{- $hsm := $top.Values.config.backend.hsm }} +{{- if $hsm.enabled }} +{{- if not $hsm.atalla.host }} +{{- fail "config.backend.hsm.atalla.host is required when config.backend.hsm.enabled is true" }} +{{- end }} +{{- if not $hsm.atalla.port }} +{{- fail "config.backend.hsm.atalla.port is required when config.backend.hsm.enabled is true" }} +{{- end }} +data: + HSM_ENABLED: "true" + HSM_SERVER_ROLE: {{ $hsm.serverRole | quote }} + {{- /* ISSUER_GRPC_HOST and MERCHANT_GRPC_HOST are omitted: the backend falls + back to HSM_GRPC_HOST, and one host serves both roles here. */}} + HSM_GRPC_HOST: {{ $hsm.grpcHost | quote }} + ISSUER_GRPC_PORT: {{ $hsm.issuerPort | quote }} + MERCHANT_GRPC_PORT: {{ $hsm.merchantPort | quote }} + ATALLA_HOST: {{ $hsm.atalla.host | quote }} + ATALLA_PORT: {{ $hsm.atalla.port | quote }} + ATALLA_TIMEOUT_MS: {{ $hsm.atalla.timeoutMs | quote }} + ATALLA_POOL_SIZE: {{ $hsm.atalla.poolSize | quote }} + ATALLA_TLS_ENABLED: {{ $hsm.atalla.tls.enabled | quote }} + {{- if $hsm.atalla.tls.enabled }} + ATALLA_SKIP_SERVER_IDENTITY_CHECK: {{ $hsm.atalla.tls.skipServerIdentityCheck | quote }} + ATALLA_CA_CERT_PATH: {{ $hsm.atalla.tls.caCertPath | quote }} + ATALLA_CLIENT_CERT_PATH: {{ $hsm.atalla.tls.clientCertPath | quote }} + ATALLA_CLIENT_KEY_PATH: {{ $hsm.atalla.tls.clientKeyPath | quote }} + {{- end }} +{{- end }} {{- end }} {{- include "common.configMapper" (list . .Values.configMaps.backend "testnet-wallet.backend.ConfigMap") }} diff --git a/helm/testnet-wallet/tests/configMap.backend_test.yaml b/helm/testnet-wallet/tests/configMap.backend_test.yaml new file mode 100644 index 000000000..e0b08185c --- /dev/null +++ b/helm/testnet-wallet/tests/configMap.backend_test.yaml @@ -0,0 +1,109 @@ +suite: testnet-wallet backend - HSM adapter configuration +templates: + - configMap.backend.yaml +set: + fullnameOverride: test-testnet-wallet +release: + name: test-testnet-wallet +tests: + # The backend rejects an empty ATALLA_HOST, so the keys must be absent + # rather than blank in every environment that does not run the adapter. + - it: 'omits every HSM key when the adapter is disabled' + asserts: + - notExists: + path: data.HSM_ENABLED + - notExists: + path: data.ATALLA_HOST + - notExists: + path: data.ISSUER_GRPC_PORT + # The contentMap keys are still there. + - equal: + path: data.NODE_ENV + value: production + + - it: 'emits the adapter block when enabled, alongside the contentMap keys' + set: + config: + backend: + hsm: + enabled: true + atalla: + host: '10.0.0.1' + port: 13506 + asserts: + - equal: + path: data.HSM_ENABLED + value: 'true' + - equal: + path: data.HSM_SERVER_ROLE + value: both + - equal: + path: data.HSM_GRPC_HOST + value: 0.0.0.0 + - equal: + path: data.ISSUER_GRPC_PORT + value: '50051' + - equal: + path: data.MERCHANT_GRPC_PORT + value: '50052' + - equal: + path: data.ATALLA_HOST + value: '10.0.0.1' + # Numbers must reach the ConfigMap as strings. + - equal: + path: data.ATALLA_PORT + value: '13506' + - equal: + path: data.ATALLA_POOL_SIZE + value: '1' + - equal: + path: data.ATALLA_CA_CERT_PATH + value: /etc/atalla/tls/ca.crt + # The override must merge with the contentMap, not replace it. + - equal: + path: data.NODE_ENV + value: production + + - it: 'drops the TLS paths when Atalla TLS is off' + set: + config: + backend: + hsm: + enabled: true + atalla: + host: '10.0.0.1' + port: 13506 + tls: + enabled: false + asserts: + - equal: + path: data.ATALLA_TLS_ENABLED + value: 'false' + - notExists: + path: data.ATALLA_CA_CERT_PATH + - notExists: + path: data.ATALLA_SKIP_SERVER_IDENTITY_CHECK + + - it: 'fails the render when the adapter is enabled without an Atalla host' + set: + config: + backend: + hsm: + enabled: true + atalla: + port: 13506 + asserts: + - failedTemplate: + errorMessage: 'config.backend.hsm.atalla.host is required when config.backend.hsm.enabled is true' + + - it: 'fails the render when the adapter is enabled without an Atalla port' + set: + config: + backend: + hsm: + enabled: true + atalla: + host: '10.0.0.1' + asserts: + - failedTemplate: + errorMessage: 'config.backend.hsm.atalla.port is required when config.backend.hsm.enabled is true' diff --git a/helm/testnet-wallet/tests/deployment.backend_test.yaml b/helm/testnet-wallet/tests/deployment.backend_test.yaml index 8eefc17bb..844ab0641 100644 --- a/helm/testnet-wallet/tests/deployment.backend_test.yaml +++ b/helm/testnet-wallet/tests/deployment.backend_test.yaml @@ -70,3 +70,44 @@ tests: - equal: path: data.GATEHUB_SEPA_ACCESS_KEY value: test-sepa-access-key + - it: 'secret references default to the Secret this chart names' + template: deployment.backend.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].env[0].valueFrom.secretKeyRef.name + value: test-testnet-wallet-backend + - it: 'secret references follow config.backend.secretName when it is set' + template: deployment.backend.yaml + set: + config: + backend: + secretName: wallet-credentials-from-operator + asserts: + - equal: + path: spec.template.spec.containers[0].env[0].valueFrom.secretKeyRef.name + value: wallet-credentials-from-operator + # The key is unchanged; only the Secret it is read from moves. + - equal: + path: spec.template.spec.containers[0].env[0].valueFrom.secretKeyRef.key + value: databaseUrl + - it: 'mounts the Atalla TLS volume when an environment supplies one' + template: deployment.backend.yaml + set: + deployments: + backend: + volumeMounts: + - name: atalla-tls + mountPath: /etc/atalla/tls + readOnly: true + volumes: + - name: atalla-tls + secret: + secretName: wallet-atalla + defaultMode: 0400 + asserts: + - equal: + path: spec.template.spec.volumes[0].secret.secretName + value: wallet-atalla + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].mountPath + value: /etc/atalla/tls diff --git a/helm/testnet-wallet/values.yaml b/helm/testnet-wallet/values.yaml index f8c519fc5..9c1c02eab 100644 --- a/helm/testnet-wallet/values.yaml +++ b/helm/testnet-wallet/values.yaml @@ -33,6 +33,15 @@ config: # already exist. shouldCreateSecrets: false + # Name of the Secret that holds the backend credentials. Empty means the + # Secret this chart names, `-backend`, which is also the one + # shouldCreateSecrets creates. + # + # Set this when the Secret comes from somewhere else — a secret operator + # writing a Secret of its own name, for example. The keys it must hold are + # the `secretKeyRef.key` values below. + secretName: '' + nodeEnv: production logLevel: info port: 4003 @@ -40,7 +49,9 @@ config: cookie: name: 'testnet.cookie' - ttl: 2630000 + # Quoted. Helm parses a bare number as float64, and the ConfigMap would + # then read `COOKIE_TTL: "2.63e+06"`. + ttl: '2630000' password: # value: "a-very-secure-random-password" secretKeyRef: name: '{{ include "common.fullname" . }}-backend' @@ -160,6 +171,52 @@ config: secretKeyRef: key: 'rafiki.adminApiSecret' + # The backend serves the issuer and merchant HSM adapter contracts over + # gRPC when `enabled` is true. One process runs both servers; `serverRole` + # chooses which. Only the cards environments run this, so it is off here. + # + # These keys reach the backend through the ConfigMap, but not through + # `configMaps.backend.contentMap`. The backend requires ATALLA_HOST and + # ATALLA_PORT when HSM_ENABLED is true and rejects an empty string for + # either, so a contentMap entry would break every environment that leaves + # them unset. templates/configMap.backend.yaml adds the whole block + # instead, and only when `enabled` is true. + # + # Turning this on is not enough by itself. Also give the backend: + # - the gRPC container ports, in `deployments.backend.ports` + # - the matching Service ports, in `services.backend.ports` + # - the Atalla TLS files, through `deployments.backend.volumes` and + # `deployments.backend.volumeMounts` + # Helm replaces a list rather than merging it, so each of those must be + # written out in full in the environment's values file. + hsm: + enabled: false + # both | issuer | merchant + serverRole: both + grpcHost: '0.0.0.0' + issuerPort: 50051 + merchantPort: 50052 + + atalla: + # Required when hsm.enabled is true. The render fails without them. + host: '' + port: '' + timeoutMs: 2000 + poolSize: 1 + tls: + enabled: true + # Skips the hostname check only. The transport still sets + # rejectUnauthorized, so the HSM certificate must chain to the CA at + # caCertPath. Needed while the backend dials an IP and the + # certificate carries no matching IP SAN. + skipServerIdentityCheck: true + # Paths, not PEM text. The backend reads each file at start-up and + # exits if one is missing, unreadable, empty, or not PEM. Mount them + # with deployments.backend.volumes. + caCertPath: /etc/atalla/tls/ca.crt + clientCertPath: /etc/atalla/tls/client.crt + clientKeyPath: /etc/atalla/tls/client.key + deployments: backend: name: backend @@ -169,81 +226,105 @@ deployments: name: test-wallet-backend # tag: "v0.1.66" # Use this to override the image tag for just this image imagePullPolicy: IfNotPresent + # Two ways to give the backend its secrets. Pick one per environment. + # + # 1. Per-key (the default below). The chart maps each environment variable + # to one key of a single Secret. `config.backend.secretName` chooses the + # Secret; `config.backend..secretKeyRef.key` chooses the key. Every + # key listed must exist: a missing key stops the pod from starting, + # unless the entry is marked `optional`. Use this when the same Secret + # holds all of them. + # + # 2. Whole-Secret. Add a `secretRef` to `envFrom` and set `env: []`. Every + # key of that Secret becomes an environment variable under its own name, + # so the Secret's keys must themselves be environment variable names, + # and keys that are absent are simply absent. Use this when the Secret + # comes from an operator that writes only the values the environment + # needs — see env/cards-playground in interledger/testnet-deploy. + # + # Helm replaces a list rather than merging it, so an environment that + # rewrites `envFrom` must repeat the configMapRef entry below. envFrom: - configMapRef: name: '{{ include "common.fullname" . }}-backend' + # Volumes for the backend pod. Empty here; the HSM adapter needs the Atalla + # TLS files, so an environment that sets config.backend.hsm.enabled must + # also fill these in. See config.backend.hsm. + volumes: [] + volumeMounts: [] + env: - name: DATABASE_URL valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.database.url.secretKeyRef.key }}' - name: COOKIE_PASSWORD valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.cookie.password.secretKeyRef.key }}' - name: AUTH_IDENTITY_SERVER_SECRET valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.identity.serverSecret.secretKeyRef.key }}' - name: REDIS_URL valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.redis.url.secretKeyRef.key }}' - name: RAFIKI_WEBHOOK_SIGNATURE_SECRET valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.webhook.signatureSecret.secretKeyRef.key }}' - name: GATEHUB_SECRET_KEY valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.gatehub.secretKey.secretKeyRef.key }}' - name: GATEHUB_SEPA_SECRET_KEY valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.gatehub.secretSepaKey.secretKeyRef.key }}' - name: GATEHUB_WEBHOOK_SECRET valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.gatehub.webhookSecret.secretKeyRef.key }}' - name: SENDGRID_API_KEY valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.email.sendgridKey.secretKeyRef.key }}' - name: ADMIN_NOTIFICATION_SECRET valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.email.adminNotificationSecret.secretKeyRef.key }}' optional: true - name: RATE_API_KEY valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.rate.apiKey.secretKeyRef.key }}' - name: STRIPE_WEBHOOK_SECRET valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.stripe.webhookSecret.secretKeyRef.key }}' - name: STRIPE_SECRET_KEY valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.stripe.secretKey.secretKeyRef.key }}' - name: ADMIN_API_SECRET valueFrom: secretKeyRef: - name: '{{ include "common.fullname" . }}-backend' + name: '{{ .Values.config.backend.secretName | default (printf "%s-backend" (include "common.fullname" .)) }}' key: '{{ .Values.config.backend.rafiki.adminApiSecret.secretKeyRef.key }}' ports: