diff --git a/charts/currents/templates/_common.tpl b/charts/currents/templates/_common.tpl index 1e692dd..349fe2c 100644 --- a/charts/currents/templates/_common.tpl +++ b/charts/currents/templates/_common.tpl @@ -192,39 +192,51 @@ ClickHouse data is loaded from an external export (scripts/org-import). value: {{ include "currents.url" (dict "context" . "input" .Values.currents.domains.recordApiHost) }} {{- end -}} -{{- define "currents.emailSMTPEnv" -}} +{{- define "currents.emailEnv" -}} +{{- $email := .Values.currents.email -}} +{{- $transporter := $email.transporter | default "smtp" -}} +{{- if not (has $transporter (list "smtp" "ses")) -}} +{{- fail (printf "currents.email.transporter must be either \"smtp\" or \"ses\", got %q" $transporter) -}} +{{- end -}} - name: EMAIL_TRANSPORTER - value: smtp -{{- if .Values.currents.email.smtp.host }} + value: {{ $transporter | quote }} +{{- if eq $transporter "ses" }} +{{- /* Credentials are resolved by the AWS SDK from the pod itself (IRSA on EKS), + so there is no secret to wire up here — only the region. */}} +- name: SES_REGION + value: {{ required "currents.email.ses.region is required when currents.email.transporter is \"ses\"" $email.ses.region | quote }} +{{- else }} +{{- if $email.smtp.host }} - name: SMTP_HOST - value: {{ .Values.currents.email.smtp.host }} + value: {{ $email.smtp.host }} {{- end }} -{{- if .Values.currents.email.smtp.port }} +{{- if $email.smtp.port }} - name: SMTP_PORT - value: {{ .Values.currents.email.smtp.port | toString | quote }} + value: {{ $email.smtp.port | toString | quote }} {{- end }} -{{- if .Values.currents.email.smtp.tls }} +{{- if $email.smtp.tls }} - name: SMTP_SECURE value: "true" {{- end }} -{{- if .Values.currents.email.smtp.secretName }} +{{- if $email.smtp.secretName }} - name: SMTP_USER valueFrom: secretKeyRef: - name: {{ .Values.currents.email.smtp.secretName }} - key: {{ .Values.currents.email.smtp.secretUserKey }} + name: {{ $email.smtp.secretName }} + key: {{ $email.smtp.secretUserKey }} {{- end }} -{{- if .Values.currents.email.smtp.secretName }} +{{- if $email.smtp.secretName }} - name: SMTP_PASS valueFrom: secretKeyRef: - name: {{ .Values.currents.email.smtp.secretName }} - key: {{ .Values.currents.email.smtp.secretPasswordKey }} + name: {{ $email.smtp.secretName }} + key: {{ $email.smtp.secretPasswordKey }} +{{- end }} {{- end }} - name: AUTOMATED_REPORTS_CURRENTS_DASHBOARD_HOSTNAME value: {{ include "currents.url" (dict "context" . "input" .Values.currents.domains.appHost) }} - name: AUTOMATED_REPORTS_EMAIL_FROM - value: {{ tpl .Values.currents.email.smtp.from . }} + value: {{ tpl (default $email.smtp.from $email.from) . }} {{- if .Values.currents.email.inviteFrom }} - name: INVITE_EMAIL_FROM value: {{ tpl .Values.currents.email.inviteFrom . }} diff --git a/charts/currents/templates/server/deployment.yaml b/charts/currents/templates/server/deployment.yaml index 942945b..aea701b 100644 --- a/charts/currents/templates/server/deployment.yaml +++ b/charts/currents/templates/server/deployment.yaml @@ -69,7 +69,7 @@ spec: {{- end }} {{- include "currents.connectionConfigEnv" . | nindent 12 }} {{- include "currents.URLConfigEnv" . | nindent 12 }} - {{- include "currents.emailSMTPEnv" . | nindent 12 }} + {{- include "currents.emailEnv" . | nindent 12 }} {{- include "currents.samlSSOEnv" . | nindent 12 }} {{- if .Values.currents.betterAuth.secretName }} - name: BETTER_AUTH_SECRET diff --git a/charts/currents/templates/writer/deployment.yaml b/charts/currents/templates/writer/deployment.yaml index f8ed3d5..45822c3 100644 --- a/charts/currents/templates/writer/deployment.yaml +++ b/charts/currents/templates/writer/deployment.yaml @@ -47,7 +47,7 @@ spec: value: "onprem" {{- include "currents.connectionConfigEnv" . | nindent 12 }} {{- include "currents.URLConfigEnv" . | nindent 12 }} - {{- include "currents.emailSMTPEnv" . | nindent 12 }} + {{- include "currents.emailEnv" . | nindent 12 }} {{- with (concat .Values.global.env .Values.writer.env) }} {{- toYaml . | nindent 12 }} {{- end }} diff --git a/charts/currents/values.yaml b/charts/currents/values.yaml index e4cb0f3..7d8eaf5 100644 --- a/charts/currents/values.yaml +++ b/charts/currents/values.yaml @@ -29,6 +29,19 @@ currents: # -- The image tag to use for the Currents images imageTag: 2026-07-26-001 email: + # -- Which transport to send outgoing email through: `smtp` or `ses`. + # With `ses` the SMTP settings are ignored and no SMTP credentials are + # needed — the AWS SDK resolves credentials from the pod itself, so grant + # the Currents service account permission to send. See + # [Using IAM Roles for Sending Email with SES](./eks/iam.md#using-iam-roles-for-sending-email-with-ses). + transporter: smtp + # -- (tpl/string) The email address to send from. Defaults to + # `currents.email.smtp.from` when unset, which is retained for compatibility. + from: "" + ses: + # -- The AWS region to send through. Required when `transporter` is `ses`, + # and the `from` address must be a verified identity in that region. + region: "" smtp: # -- The SMTP server port to use port: 587 @@ -40,10 +53,11 @@ currents: secretPasswordKey: password # -- (tpl/string) The email address to send from from: "Currents Report " - # -- the SMTP server to use + # -- the SMTP server to use. Required unless `transporter` is `ses`. # @section -- Required host: "" - # -- K8s secret to use for the SMTP username/password + # -- K8s secret to use for the SMTP username/password. Required unless + # `transporter` is `ses`. # @section -- Required secretName: "" # -- Whether the SMTP server uses TLS diff --git a/docs/configuration.md b/docs/configuration.md index d568157..5cec96e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -19,8 +19,8 @@ The following table lists the configurable parameters of the `currents` chart an | currents.domains.appHost | string | `"currents-app.localhost"` | The host for the app | | currents.domains.recordApiHost | string | `"currents-record.localhost"` | The host for the recording endpoint that the test reporters communicate with | | currents.rootUser.password.secretName | string | `""` | The K8s secret containing the root user password. The password is used during initial setup only. | -| currents.email.smtp.host | string | `""` | the SMTP server to use | -| currents.email.smtp.secretName | string | `""` | K8s secret to use for the SMTP username/password | +| currents.email.smtp.host | string | `""` | the SMTP server to use. Required unless `transporter` is `ses`. | +| currents.email.smtp.secretName | string | `""` | K8s secret to use for the SMTP username/password. Required unless `transporter` is `ses`. | | currents.betterAuth.secretName | string | `""` | The K8s secret containing the Better Auth secret | | currents.apiInternalToken.secretName | string | `""` | The K8s secret to use for the internal API token | | currents.clickhouse.user.secretName | string | `""` | The k8s secret to use for the ClickHouse password | @@ -89,6 +89,9 @@ The following table lists the configurable parameters of the `currents` chart an |-----|------|---------|-------------| | currents.rootUser.email | string | `"admin@{{ .Values.currents.domains.appHost }}"` | The email address of the root user | | currents.imageTag | string | `"2026-07-26-001"` | The image tag to use for the Currents images | +| currents.email.transporter | string | `"smtp"` | Which transport to send outgoing email through: `smtp` or `ses`. With `ses` the SMTP settings are ignored and no SMTP credentials are needed — the AWS SDK resolves credentials from the pod itself, so grant the Currents service account permission to send. See [Using IAM Roles for Sending Email with SES](./eks/iam.md#using-iam-roles-for-sending-email-with-ses). | +| currents.email.from | tpl/string | `""` | The email address to send from. Defaults to `currents.email.smtp.from` when unset, which is retained for compatibility. | +| currents.email.ses.region | string | `""` | The AWS region to send through. Required when `transporter` is `ses`, and the `from` address must be a verified identity in that region. | | currents.email.smtp.port | int | `587` | The SMTP server port to use | | currents.email.smtp.from | tpl/string | `"Currents Report "` | The email address to send from | | currents.email.smtp.tls | bool | `false` | Whether the SMTP server uses TLS | diff --git a/docs/eks/iam.md b/docs/eks/iam.md index 165ca7f..bf04eab 100644 --- a/docs/eks/iam.md +++ b/docs/eks/iam.md @@ -161,3 +161,116 @@ After the pods restart, describe one of the Currents pods to confirm the `AWS_WE ```shell kubectl describe pod ``` + +## Using IAM Roles for Sending Email with SES + +By default Currents sends outgoing email (invitations, automated reports) over SMTP, +which requires storing SMTP credentials in a Kubernetes secret. If you'd rather not +hold those credentials in the cluster, Currents can send through +[Amazon SES](https://docs.aws.amazon.com/ses/latest/dg/Welcome.html) instead and +authenticate with an IAM role — no email secret required. + +Prerequisites: + +- An EKS cluster with an [IAM OIDC Provider enabled](https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html) (same requirement as IRSA for object storage, above). +- A **verified identity** in SES — the domain or address you send from — in the region you plan to use. +- If your SES account is still in the [sandbox](https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html), every recipient must also be verified. Request production access before relying on this for real reports. + +**1. Create the IAM Policy** + +Replace `us-east-1`, `111122223333`, and the identity name with your own. Scoping to +the identity is optional but recommended; `"Resource": "*"` also works. + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ses:SendEmail", + "ses:SendRawEmail" + ], + "Resource": "arn:aws:ses:us-east-1:111122223333:identity/example.com" + } + ] +} +``` + +**2. Attach the Policy to the Currents Service Account** + +Currents uses a single service account for all of its pods, so if you already set up +IRSA for object storage you can attach this policy to that same role rather than +creating a second one: + +```shell +aws iam attach-role-policy --role-name currents-irsa --policy-arn +``` + +If you have not set up IRSA yet, follow steps 2–4 of +[Using IAM Roles for Accessing Object Storage](#using-iam-roles-for-accessing-object-storage) +to create and associate the role, attaching this policy instead of (or in addition +to) the S3 one. + +**3. Switch the Chart to the SES Transport** + +In your `currents-helm-config.yaml`: + +```yaml +currents: + email: + transporter: ses + # Must be a verified SES identity in the region below + from: "Currents Report " + ses: + region: us-east-1 +``` + +With `transporter: ses` the SMTP settings are ignored — `currents.email.smtp.host` +and `currents.email.smtp.secretName` are no longer required, and you do not need to +create the `currents-email-smtp` secret. The other email settings +(`inviteFrom`, `inviteBcc`, `reportsBcc`, `inviteExpirationDays`, `linksBaseUrl`) +apply to both transports. + +Apply it: + +```shell +helm upgrade --install currents currents --repo https://currents-dev.github.io/helm-charts/ -f currents-helm-config.yaml +``` + +**4. Confirm the Pods Picked Up the Change** + +Email is sent by the `server` pod (invitations) and the `writer` pod (automated +reports). Confirm both see the SES settings and the IRSA credentials: + +```shell +kubectl exec deploy/currents-writer -- env | grep -E 'EMAIL_TRANSPORTER|SES_REGION|AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE' +``` + +Expected output: + +``` +EMAIL_TRANSPORTER=ses +SES_REGION=us-east-1 +AWS_ROLE_ARN=arn:aws:iam::111122223333:role/currents-irsa +AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token +``` + +If `EMAIL_TRANSPORTER` is still `smtp`, the values did not reach the pod — re-run the +upgrade with your full values file rather than `--reuse-values`, and confirm the +deployment rolled. + +**5. Verify Delivery** + +Trigger an email (for example, invite a user) and check the logs: + +```shell +kubectl logs deploy/currents-writer --tail=100 | grep -i email +``` + +> **Note:** a successful send logs `Message sent`. Delivery failures are logged as +> `Error sending email with SES` — but they are **followed by a misleading +> `Email sent` line**, so treat the presence of an `Error sending email with SES` +> entry as a failure regardless of what comes after it. Common causes are an +> unverified `from` identity, a region mismatch, or SES sandbox restrictions on the +> recipient.