diff --git a/bots/quoter-bot/helm/quoter-bot/Chart.yaml b/bots/quoter-bot/helm/quoter-bot/Chart.yaml index 881278f..d50acde 100644 --- a/bots/quoter-bot/helm/quoter-bot/Chart.yaml +++ b/bots/quoter-bot/helm/quoter-bot/Chart.yaml @@ -7,7 +7,7 @@ description: >- Morpho Midnight maker bot: validates Base and Midnight setup, bootstraps target lending positions, and maintains two-sided rate ladders from one long-running container. type: application -version: 0.3.0 +version: 0.4.0 # Default image tag. The Docker Hub repository morphoorg/quoter publishes `latest` plus one # immutable tag per release commit hash; pin a commit tag through `image.tag` for reproducible # deployments. diff --git a/bots/quoter-bot/helm/quoter-bot/README.md b/bots/quoter-bot/helm/quoter-bot/README.md index b95a867..b72eb2a 100644 --- a/bots/quoter-bot/helm/quoter-bot/README.md +++ b/bots/quoter-bot/helm/quoter-bot/README.md @@ -171,6 +171,39 @@ files over `--set` for the `config` block for the same reason. - To keep the whole file out of Helm release storage, pre-create a Secret with the complete configuration under the key `quoter-bot.yaml` and set `existingConfigSecret`. +## Runtime Secret from AWS Secrets Manager + +`externalSecret.enabled` replaces the hand-applied runtime Secret with an +[External Secrets Operator](https://external-secrets.io) sync: the chart renders a namespaced +`SecretStore` (AWS Secrets Manager, authenticating as the chart-managed ServiceAccount through +`auth.jwt.serviceAccountRef`) and an `ExternalSecret` `-runtime` that `dataFrom` +extracts a flat JSON object of environment variables from `externalSecret.remoteKey` (a secret +name or ARN, e.g. `quoter-bot/prd/1/runtime`). The rendered Secret is appended to `envFrom` +after any `envFrom` entries, so its keys override `config` like any other environment variable. + +Prerequisites: + +- External Secrets Operator installed in the cluster (`external-secrets.io/v1` CRDs). +- `serviceAccount.create: true` with an IRSA or EKS Pod Identity annotation; that IAM role + needs `secretsmanager:GetSecretValue`/`DescribeSecret` on `remoteKey` and `kms:Decrypt` on + the secret's key — no other Secrets Manager or KMS access. +- Optional [Stakater Reloader](https://github.com/stakater/Reloader): the StatefulSet is + annotated `secret.reloader.stakater.com/reload: -runtime`, so a rotated value + restarts the pod automatically (environment variables are captured at container start). + Without Reloader, a rotation still syncs into the Secret on `refreshInterval` but the pod + needs a manual `kubectl rollout restart`. + +```yaml +serviceAccount: + create: true + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam:::role/tools-quoter-bot-- +externalSecret: + enabled: true + remoteKey: quoter-bot/prd/1/runtime + region: eu-west-3 +``` + ## Parameters ### Image and workload @@ -202,6 +235,7 @@ files over `--set` for the `config` block for the same reason. | `existingConfigSecret` | `''` | Pre-created Secret with the full file under key `quoter-bot.yaml`; replaces the rendered Secret. | | `env` | `[]` | Extra `EnvVar` objects; environment overrides YAML (signer secret, `BETTERSTACK_*`). `XDG_STATE_HOME` is reserved and filtered out. | | `envFrom` | `[]` | Extra `EnvFromSource` objects for whole Secrets/ConfigMaps of overrides. | +| `externalSecret` | off | Fetch the runtime environment Secret from AWS Secrets Manager via External Secrets Operator — see below. Requires `serviceAccount.create: true` with an IRSA/Pod Identity annotation; the Secret becomes `-runtime` and is appended to `envFrom`. | ### Persistence and security diff --git a/bots/quoter-bot/helm/quoter-bot/templates/NOTES.txt b/bots/quoter-bot/helm/quoter-bot/templates/NOTES.txt index faad22a..d2c4b07 100644 --- a/bots/quoter-bot/helm/quoter-bot/templates/NOTES.txt +++ b/bots/quoter-bot/helm/quoter-bot/templates/NOTES.txt @@ -12,6 +12,12 @@ WARNING: no `config` mapping and no `existingConfigSecret` were provided. The bo environment-only mode and fails loudly unless `env`/`envFrom` supply every required variable. {{- end }} +{{- if .Values.externalSecret.enabled }} +Runtime environment: ExternalSecret {{ include "quoter-bot.runtimeSecretName" . }} syncs +{{ .Values.externalSecret.remoteKey }} from AWS Secrets Manager every +{{ .Values.externalSecret.refreshInterval }}; Stakater Reloader restarts the pod on rotation. +{{- end }} + {{- if not .Values.persistence.enabled }} WARNING: persistence is disabled. Durable offer-group ownership state is lost on every diff --git a/bots/quoter-bot/helm/quoter-bot/templates/_helpers.tpl b/bots/quoter-bot/helm/quoter-bot/templates/_helpers.tpl index f9e4796..e71fb67 100644 --- a/bots/quoter-bot/helm/quoter-bot/templates/_helpers.tpl +++ b/bots/quoter-bot/helm/quoter-bot/templates/_helpers.tpl @@ -133,3 +133,13 @@ keeping the pin unique even when long release names share their truncated prefix {{- define "quoter-bot.configYaml" -}} {{- toYaml .Values.config }} {{- end }} + +{{/* Name of the ExternalSecret-owned runtime environment Secret. */}} +{{- define "quoter-bot.runtimeSecretName" -}} +{{- $fullname := include "quoter-bot.fullname" . }} +{{- if gt (len $fullname) 55 }} +{{- printf "%s-%s-runtime" ($fullname | trunc 46 | trimSuffix "-") (sha256sum $fullname | trunc 8) }} +{{- else }} +{{- printf "%s-runtime" $fullname }} +{{- end }} +{{- end }} diff --git a/bots/quoter-bot/helm/quoter-bot/templates/externalsecret.yaml b/bots/quoter-bot/helm/quoter-bot/templates/externalsecret.yaml new file mode 100644 index 0000000..d304d13 --- /dev/null +++ b/bots/quoter-bot/helm/quoter-bot/templates/externalsecret.yaml @@ -0,0 +1,23 @@ +{{- if .Values.externalSecret.enabled }} +# Extracts the whole Secrets Manager value as a flat JSON object of environment variables. +# creationPolicy Owner garbage-collects the rendered Secret with this ExternalSecret; +# deletionPolicy Retain keeps it (and the pod's env) if the ExternalSecret itself is removed. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ include "quoter-bot.runtimeSecretName" . | quote }} + labels: + {{- include "quoter-bot.labels" . | nindent 4 }} +spec: + refreshInterval: {{ .Values.externalSecret.refreshInterval }} + secretStoreRef: + name: {{ include "quoter-bot.fullname" . }}-aws + kind: SecretStore + target: + name: {{ include "quoter-bot.runtimeSecretName" . | quote }} + creationPolicy: Owner + deletionPolicy: Retain + dataFrom: + - extract: + key: {{ .Values.externalSecret.remoteKey | quote }} +{{- end }} diff --git a/bots/quoter-bot/helm/quoter-bot/templates/secretstore.yaml b/bots/quoter-bot/helm/quoter-bot/templates/secretstore.yaml new file mode 100644 index 0000000..670ea4c --- /dev/null +++ b/bots/quoter-bot/helm/quoter-bot/templates/secretstore.yaml @@ -0,0 +1,28 @@ +{{- if .Values.externalSecret.enabled }} +{{- if not .Values.serviceAccount.create }} +{{- fail "externalSecret.enabled requires serviceAccount.create: the SecretStore authenticates as the chart-managed ServiceAccount (auth.jwt.serviceAccountRef)" }} +{{- end }} +{{- if not .Values.externalSecret.remoteKey }} +{{- fail "externalSecret.enabled requires externalSecret.remoteKey: the AWS Secrets Manager secret name or ARN" }} +{{- end }} +{{- if not .Values.externalSecret.region }} +{{- fail "externalSecret.enabled requires externalSecret.region: the AWS region of remoteKey" }} +{{- end }} +# Namespaced SecretStore authenticating as the chart-managed ServiceAccount; IRSA or EKS Pod +# Identity turns its projected token into the bot's IAM role. +apiVersion: external-secrets.io/v1 +kind: SecretStore +metadata: + name: {{ include "quoter-bot.fullname" . }}-aws + labels: + {{- include "quoter-bot.labels" . | nindent 4 }} +spec: + provider: + aws: + service: SecretsManager + region: {{ .Values.externalSecret.region | quote }} + auth: + jwt: + serviceAccountRef: + name: {{ include "quoter-bot.serviceAccountName" . | quote }} +{{- end }} diff --git a/bots/quoter-bot/helm/quoter-bot/templates/statefulset.yaml b/bots/quoter-bot/helm/quoter-bot/templates/statefulset.yaml index d652ccb..119cccb 100644 --- a/bots/quoter-bot/helm/quoter-bot/templates/statefulset.yaml +++ b/bots/quoter-bot/helm/quoter-bot/templates/statefulset.yaml @@ -25,6 +25,13 @@ metadata: name: {{ include "quoter-bot.fullname" . | quote }} labels: {{- include "quoter-bot.labels" . | nindent 4 }} + {{- if .Values.externalSecret.enabled }} + annotations: + # Annotation lives on the workload, not the pod template: Stakater Reloader rolls the + # StatefulSet itself when the ExternalSecret-owned Secret changes (env is captured at + # container start, so a rotated value must restart the pod to take effect). + secret.reloader.stakater.com/reload: {{ include "quoter-bot.runtimeSecretName" . | quote }} + {{- end }} spec: # The bot is a singleton writer: its nonce cursor, serialized mutation queue, and durable # offer-group ownership state are per-instance, so two replicas against one maker on the same @@ -195,7 +202,9 @@ spec: {{- with $extraEnv }} {{- toYaml . | nindent 12 }} {{- end }} - {{- with .Values.envFrom }} + {{- $runtimeEnvFrom := ternary (list (dict "secretRef" (dict "name" (include "quoter-bot.runtimeSecretName" .)))) (list) .Values.externalSecret.enabled }} + {{- $envFrom := concat .Values.envFrom $runtimeEnvFrom }} + {{- with $envFrom }} envFrom: {{- toYaml . | nindent 12 }} {{- end }} diff --git a/bots/quoter-bot/helm/quoter-bot/values.yaml b/bots/quoter-bot/helm/quoter-bot/values.yaml index eedd96f..17307a6 100644 --- a/bots/quoter-bot/helm/quoter-bot/values.yaml +++ b/bots/quoter-bot/helm/quoter-bot/values.yaml @@ -165,6 +165,22 @@ envFrom: [] # - secretRef: # name: quoter-bot-signer +# Deliver the runtime environment Secret from AWS Secrets Manager through External Secrets +# Operator (external-secrets.io/v1) instead of a hand-applied Secret. Requires +# serviceAccount.create: true with an IRSA/Pod Identity annotation: the SecretStore +# authenticates as that ServiceAccount (auth.jwt.serviceAccountRef), so the bot's own IAM role +# needs secretsmanager:GetSecretValue/DescribeSecret on `remoteKey` and kms:Decrypt on its key. +# The remote value is a flat JSON object of environment variables; every key becomes a Secret +# key of -runtime, which the chart appends to envFrom. The StatefulSet is annotated +# `secret.reloader.stakater.com/reload: -runtime`, so with Stakater Reloader installed a +# rotated value restarts the pod (environment variables are captured at container start). +externalSecret: + enabled: false + # Secrets Manager secret name or ARN, e.g. quoter-bot/prd/1/runtime. + remoteKey: '' + region: '' + refreshInterval: 5m + # Durable ownership state (bot-issued offer-group IDs) lives under this mount; the chart # always sets XDG_STATE_HOME to `mountPath`. Losing it makes previously bot-issued groups # unknown, which fails readiness until an operator invalidates or adopts them — keep