diff --git a/charts/spire/charts/spiffe-oidc-discovery-provider/README.md b/charts/spire/charts/spiffe-oidc-discovery-provider/README.md index 1b69558..8a15e26 100644 --- a/charts/spire/charts/spiffe-oidc-discovery-provider/README.md +++ b/charts/spire/charts/spiffe-oidc-discovery-provider/README.md @@ -51,6 +51,7 @@ A Helm chart to install the SPIFFE OIDC discovery provider. | `service.ports.http` | Insecure port for the service | `80` | | `service.ports.https` | Secure port for the service | `443` | | `service.annotations` | Annotations for service resource | `{}` | +| `service.loadBalancerIP` | IP address to assign to load balancer (if supported) | `""` | | `configMap.annotations` | Annotations to add to the SPIFFE OIDC Discovery Provider ConfigMap | `{}` | | `podSecurityContext` | Pod security context for OIDC discovery provider pods | `{}` | | `securityContext` | Security context for OIDC discovery provider deployment | `{}` | diff --git a/charts/spire/charts/spiffe-oidc-discovery-provider/templates/service.yaml b/charts/spire/charts/spiffe-oidc-discovery-provider/templates/service.yaml index fb8de10..91d37ee 100644 --- a/charts/spire/charts/spiffe-oidc-discovery-provider/templates/service.yaml +++ b/charts/spire/charts/spiffe-oidc-discovery-provider/templates/service.yaml @@ -9,6 +9,9 @@ metadata: {{- end }} spec: type: {{ .Values.service.type }} + {{- if and (eq .Values.service.type "LoadBalancer") .Values.service.loadBalancerIP }} + loadBalancerIP: {{ .Values.service.loadBalancerIP }} + {{- end }} ports: {{- if eq (include "spiffe-oidc-discovery-provider.tls-enabled" .) "false" }} - name: http diff --git a/charts/spire/charts/spiffe-oidc-discovery-provider/values.yaml b/charts/spire/charts/spiffe-oidc-discovery-provider/values.yaml index 79d1c11..e635da9 100644 --- a/charts/spire/charts/spiffe-oidc-discovery-provider/values.yaml +++ b/charts/spire/charts/spiffe-oidc-discovery-provider/values.yaml @@ -88,6 +88,8 @@ service: https: 443 annotations: {} # external-dns.alpha.kubernetes.io/hostname: oidc-discovery.example.org + ## @param service.loadBalancerIP IP address to assign to load balancer (if supported) + loadBalancerIP: "" configMap: ## @param configMap.annotations [object] Annotations to add to the SPIFFE OIDC Discovery Provider ConfigMap diff --git a/charts/spire/charts/spire-server/README.md b/charts/spire/charts/spire-server/README.md index 45e3da5..c65866b 100644 --- a/charts/spire/charts/spire-server/README.md +++ b/charts/spire/charts/spire-server/README.md @@ -104,6 +104,7 @@ In order to run Tornjak with simple HTTP Connection only, make sure you don't cr | `service.type` | Type of the Spire server service created | `ClusterIP` | | `service.port` | Port for the created service | `443` | | `service.annotations` | Annotations to add to the service object | `{}` | +| `service.loadBalancerIP` | IP address to assign to load balancer (if supported) | `""` | | `configMap.annotations` | Annotations to add to the SPIRE Server ConfigMap | `{}` | | `resources` | Resource requests and limits | `{}` | | `autoscaling.enabled` | Flag to enable autoscaling | `false` | @@ -125,7 +126,7 @@ In order to run Tornjak with simple HTTP Connection only, make sure you don't cr | `persistence.accessMode` | What access mode to use for persistence. Valid options are ReadWriteOnce (recommended), ReadWriteOncePod, ReadWriteMany (not recommended) | `ReadWriteOnce` | | `persistence.storageClass` | What storage class to use for persistence | `nil` | | `persistence.hostPath` | Which path to use on the host when persistence.type = hostPath | `""` | -| `dataStore.sql.databaseType` | Other supported databases are ["postgres", "mysql", "aws_postgresql", "aws_mysql"]. Note: aws type databases are still experimental | `sqlite3` | +| `dataStore.sql.databaseType` | Other supported databases are ["postgres", "mysql", "aws_postgresql", "aws_mysql", "gcp_mysql_sa_iam"]. Note: aws type databases are still experimental. gcp_mysql_sa_iam uses IAM authentication by default. | `sqlite3` | | `dataStore.sql.databaseName` | Only used when type != "sqlite3" | `spire` | | `dataStore.sql.host` | Only used when type != "sqlite3" | `""` | | `dataStore.sql.port` | If 0 (default), it will auto set to 5432 for postgres and 3306 for mysql. Only used by those databases. | `0` | diff --git a/charts/spire/charts/spire-server/templates/_helpers.tpl b/charts/spire/charts/spire-server/templates/_helpers.tpl index c2c755e..f590dd6 100644 --- a/charts/spire/charts/spire-server/templates/_helpers.tpl +++ b/charts/spire/charts/spire-server/templates/_helpers.tpl @@ -234,21 +234,35 @@ Create the name of the service account to use {{- $_ := set $config "database_type" "sqlite3" }} {{- $query := include "spire-server.config-sqlite-query" .Values.dataStore.sql.options }} {{- $_ := set $config "connection_string" (printf "%s%s" .Values.dataStore.sql.file $query) }} -{{- else if or (eq .Values.dataStore.sql.databaseType "mysql") (eq .Values.dataStore.sql.databaseType "aws_mysql") }} +{{- else if or (eq .Values.dataStore.sql.databaseType "mysql") (eq .Values.dataStore.sql.databaseType "aws_mysql") (eq .Values.dataStore.sql.databaseType "gcp_mysql_sa_iam") }} {{- if eq .Values.dataStore.sql.databaseType "mysql" }} {{- $_ := set $config "database_type" "mysql" }} {{- $pw = "${DBPW}" }} {{- $ropw = "${RODBPW}" }} + {{- else if eq .Values.dataStore.sql.databaseType "gcp_mysql_sa_iam" }} + {{- $_ := set $config "database_type" "mysql" }} + {{- $pw = "" }} + {{- $ropw = "" }} {{- else }} {{- $_ := set $config "database_type" (list (dict "aws_mysql" (dict "region" .Values.dataStore.sql.region))) }} - {{- end }} + {{- $pw = "${DBPW}" }} + {{- $ropw = "${RODBPW}" }} + {{- end }} {{- $port := int .Values.dataStore.sql.port | default 3306 }} {{- $query := include "spire-server.config-mysql-query" .Values.dataStore.sql.options }} - {{- $_ := set $config "connection_string" (printf "%s:%s@tcp(%s:%d)/%s%s" .Values.dataStore.sql.username $pw .Values.dataStore.sql.host $port .Values.dataStore.sql.databaseName $query) }} + {{- if eq $pw "" }} + {{- $_ := set $config "connection_string" (printf "%s@tcp(%s:%d)/%s%s" .Values.dataStore.sql.username .Values.dataStore.sql.host $port .Values.dataStore.sql.databaseName $query) }} + {{- else }} + {{- $_ := set $config "connection_string" (printf "%s:%s@tcp(%s:%d)/%s%s" .Values.dataStore.sql.username $pw .Values.dataStore.sql.host $port .Values.dataStore.sql.databaseName $query) }} + {{- end }} {{- if .Values.dataStore.sql.readOnly.enabled }} {{- $roPort := int .Values.dataStore.sql.readOnly.port | default 3306 }} {{- $roQuery := include "spire-server.config-mysql-query" .Values.dataStore.sql.readOnly.options }} - {{- $_ := set $config "ro_connection_string" (printf "%s:%s@tcp(%s:%d)/%s%s" .Values.dataStore.sql.readOnly.username $ropw .Values.dataStore.sql.readOnly.host $roPort .Values.dataStore.sql.readOnly.databaseName $roQuery) }} + {{- if eq $ropw "" }} + {{- $_ := set $config "ro_connection_string" (printf "%s@tcp(%s:%d)/%s%s" .Values.dataStore.sql.readOnly.username .Values.dataStore.sql.readOnly.host $roPort .Values.dataStore.sql.readOnly.databaseName $roQuery) }} + {{- else }} + {{- $_ := set $config "ro_connection_string" (printf "%s:%s@tcp(%s:%d)/%s%s" .Values.dataStore.sql.readOnly.username $ropw .Values.dataStore.sql.readOnly.host $roPort .Values.dataStore.sql.readOnly.databaseName $roQuery) }} + {{- end }} {{- end }} {{- else if or (eq .Values.dataStore.sql.databaseType "postgres") (eq .Values.dataStore.sql.databaseType "aws_postgres") }} {{- if eq .Values.dataStore.sql.databaseType "postgres" }} diff --git a/charts/spire/charts/spire-server/templates/secret.yaml b/charts/spire/charts/spire-server/templates/secret.yaml index 8ba00a2..b0baa59 100644 --- a/charts/spire/charts/spire-server/templates/secret.yaml +++ b/charts/spire/charts/spire-server/templates/secret.yaml @@ -7,8 +7,7 @@ {{- if and (.Values.dataStore.sql.externalSecret.enabled) (eq .Values.dataStore.sql.externalSecret.key "") }} {{- fail "dataStore.sql.externalSecret.key cannot be empty string when dataStore.sql.externalSecret is enabled" }} {{- end }} -{{- if ne .Values.dataStore.sql.databaseType "sqlite3" }} -{{- if not .Values.dataStore.sql.externalSecret.enabled }} +{{- if and (ne .Values.dataStore.sql.databaseType "sqlite3") (not .Values.dataStore.sql.externalSecret.enabled) (ne .Values.dataStore.sql.databaseType "gcp_mysql_sa_iam") }} apiVersion: v1 kind: Secret metadata: @@ -20,4 +19,3 @@ data: RODBPW: {{ .Values.dataStore.sql.readOnly.password | b64enc }} {{- end }} {{- end }} -{{- end }} diff --git a/charts/spire/charts/spire-server/templates/server-resource.yaml b/charts/spire/charts/spire-server/templates/server-resource.yaml index 51087bd..f790eff 100644 --- a/charts/spire/charts/spire-server/templates/server-resource.yaml +++ b/charts/spire/charts/spire-server/templates/server-resource.yaml @@ -228,7 +228,7 @@ spec: {{- with .Values.extraEnv }} {{- . | toYaml | nindent 10 }} {{- end }} - {{- if ne .Values.dataStore.sql.databaseType "sqlite3" }} + {{- if and (ne .Values.dataStore.sql.databaseType "sqlite3") (ne .Values.dataStore.sql.databaseType "gcp_mysql_sa_iam") }} {{- if .Values.dataStore.sql.externalSecret.enabled }} - name: DBPW valueFrom: @@ -242,13 +242,14 @@ spec: name: {{ $fullname }}-dbpw key: DBPW {{- end }} - {{- if and .Values.dataStore.sql.readOnly.enabled .Values.dataStore.sql.readOnly.externalSecret.enabled }} + {{- if and .Values.dataStore.sql.readOnly.enabled (ne .Values.dataStore.sql.databaseType "gcp_mysql_sa_iam") }} + {{- if .Values.dataStore.sql.readOnly.externalSecret.enabled }} - name: RODBPW valueFrom: secretKeyRef: name: {{ .Values.dataStore.sql.readOnly.externalSecret.name }} key: {{ .Values.dataStore.sql.readOnly.externalSecret.key }} - {{- else if .Values.dataStore.sql.readOnly.enabled }} + {{- else }} - name: RODBPW valueFrom: secretKeyRef: @@ -256,6 +257,7 @@ spec: key: RODBPW {{- end }} {{- end }} + {{- end }} {{- if ne .Values.keyManager.awsKMS.accessKeyID "" }} - name: AWS_KMS_ACCESS_KEY_ID valueFrom: diff --git a/charts/spire/charts/spire-server/templates/service.yaml b/charts/spire/charts/spire-server/templates/service.yaml index b9726ef..22875cd 100644 --- a/charts/spire/charts/spire-server/templates/service.yaml +++ b/charts/spire/charts/spire-server/templates/service.yaml @@ -12,6 +12,9 @@ metadata: {{- include "spire-server.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} + {{- if and (eq .Values.service.type "LoadBalancer") .Values.service.loadBalancerIP }} + loadBalancerIP: {{ .Values.service.loadBalancerIP }} + {{- end }} ports: - name: grpc port: {{ .Values.service.port }} diff --git a/charts/spire/charts/spire-server/values.yaml b/charts/spire/charts/spire-server/values.yaml index 2b94cb8..7271c8f 100644 --- a/charts/spire/charts/spire-server/values.yaml +++ b/charts/spire/charts/spire-server/values.yaml @@ -80,6 +80,8 @@ service: type: ClusterIP port: 443 annotations: {} + ## @param service.loadBalancerIP IP address to assign to load balancer (if supported) + loadBalancerIP: "" configMap: ## @param configMap.annotations [object] Annotations to add to the SPIRE Server ConfigMap @@ -155,7 +157,7 @@ persistence: dataStore: sql: - ## @param dataStore.sql.databaseType Other supported databases are ["postgres", "mysql", "aws_postgresql", "aws_mysql"]. Note: aws type databases are still experimental + ## @param dataStore.sql.databaseType Other supported databases are ["postgres", "mysql", "aws_postgresql", "aws_mysql", "gcp_mysql_sa_iam"]. Note: aws type databases are still experimental. gcp_mysql_sa_iam uses IAM authentication by default. databaseType: sqlite3 ## @param dataStore.sql.databaseName Only used when type != "sqlite3" databaseName: spire diff --git a/examples/cloud-sql-proxy-gcp/README.md b/examples/cloud-sql-proxy-gcp/README.md new file mode 100644 index 0000000..9e8a7d8 --- /dev/null +++ b/examples/cloud-sql-proxy-gcp/README.md @@ -0,0 +1,46 @@ +# Cloud SQL Proxy with GCP IAM Authentication + +Use SPIRE Server with Google Cloud SQL using IAM authentication instead of passwords. + +## Setup + +### 1. Create Infrastructure + +**Prerequisites:** +- GKE cluster with Workload Identity enabled +- Terraform configured with GCP provider + +Use Terraform to create the database, service account, and Workload Identity: + +```bash +# Edit main.tf and replace placeholders: +# - YOUR_PROJECT_ID with your GCP project ID +# - YOUR_REGION with your preferred region (e.g., us-central1) + +terraform init +terraform apply +``` + +**Note:** This creates: +- Service account with Cloud SQL Client and Instance User roles +- Cloud SQL instance with IAM authentication enabled +- Kubernetes service account with Workload Identity annotation +- IAM binding for Workload Identity + +### 2. Deploy + +Edit `values.yaml` with your project details, then: + +```bash +helm upgrade --install -n spire spire spire \ + --repo https://spiffe.github.io/helm-charts-hardened/ \ + -f values.yaml +``` + +## How It Works + +1. Cloud SQL Proxy runs as an init container with `restartPolicy: Always` +2. Proxy connects to your database using IAM authentication +3. SPIRE connects to `127.0.0.1:3306` through the proxy +4. Uses `gcp_mysql_sa_iam` database type for automatic IAM authentication +5. No passwords needed - everything uses IAM authentication \ No newline at end of file diff --git a/examples/cloud-sql-proxy-gcp/main.tf b/examples/cloud-sql-proxy-gcp/main.tf new file mode 100644 index 0000000..15cb5a9 --- /dev/null +++ b/examples/cloud-sql-proxy-gcp/main.tf @@ -0,0 +1,71 @@ +# Create service account for SPIRE +resource "google_service_account" "spire" { + account_id = "sa-spire" + display_name = "SPIRE Server Service Account" + project = "YOUR_PROJECT_ID" +} + +# Grant Cloud SQL Client role +resource "google_project_iam_member" "cloudsql_client" { + project = "YOUR_PROJECT_ID" + role = "roles/cloudsql.client" + member = "serviceAccount:${google_service_account.spire.email}" +} + +# Grant Cloud SQL Instance User role for IAM authentication +resource "google_project_iam_member" "cloudsql_instance_user" { + project = "YOUR_PROJECT_ID" + role = "roles/cloudsql.instanceUser" + member = "serviceAccount:${google_service_account.spire.email}" +} + +# Create Cloud SQL database instance +resource "google_sql_database_instance" "instance" { + name = "spire-db" + region = "YOUR_REGION" + database_version = "MYSQL_8_0" + project = "YOUR_PROJECT_ID" + + settings { + tier = "db-f1-micro" + database_flags { + name = "cloudsql_iam_authentication" + value = "on" + } + } + + deletion_protection = true +} + +# Create database +resource "google_sql_database" "database" { + name = "spire" + instance = google_sql_database_instance.instance.name + project = "YOUR_PROJECT_ID" +} + +# Create IAM user for the service account +resource "google_sql_user" "iam_service_account_user" { + name = google_service_account.spire.email + instance = google_sql_database_instance.instance.name + type = "CLOUD_IAM_SERVICE_ACCOUNT" + project = "YOUR_PROJECT_ID" +} + +# Create Kubernetes service account +resource "kubernetes_service_account" "spire" { + metadata { + name = "sa-spire" + namespace = "spire" + annotations = { + "iam.gke.io/gcp-service-account" = google_service_account.spire.email + } + } +} + +# Set up Workload Identity binding +resource "google_service_account_iam_member" "workload_identity_user" { + service_account_id = google_service_account.spire.name + role = "roles/iam.workloadIdentityUser" + member = "serviceAccount:YOUR_PROJECT_ID.svc.id.goog[spire/sa-spire]" +} \ No newline at end of file diff --git a/examples/cloud-sql-proxy-gcp/values.yaml b/examples/cloud-sql-proxy-gcp/values.yaml new file mode 100644 index 0000000..d5259cf --- /dev/null +++ b/examples/cloud-sql-proxy-gcp/values.yaml @@ -0,0 +1,57 @@ +global: + spire: + clusterName: gke_example-cluster_us-central1-a_example-cluster + trustDomain: example.org + caSubject: + country: ARPA + organization: Example + commonName: example.org + +spire-server: + serviceAccount: + create: true + annotations: + iam.gke.io/gcp-service-account: "sa-spire@PROJECT_ID.iam.gserviceaccount.com" + name: "sa-spire" + + initContainers: + - name: cloud-sql-proxy + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.14.1 + restartPolicy: Always + args: + - "--auto-iam-authn" + - "--structured-logs" + - "--port=3306" + - "PROJECT_ID:REGION:INSTANCE_NAME" + env: + - name: GOOGLE_CLOUD_PROJECT + value: "PROJECT_ID" + - name: GOOGLE_CLOUD_REGION + value: "REGION" + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "500m" + + dataStore: + sql: + databaseType: gcp_mysql_sa_iam + databaseName: spire + host: 127.0.0.1 + port: 3306 + username: "sa-spire" + password: "" + externalSecret: + enabled: false \ No newline at end of file