Skip to content

Deploy SPIRE

This page provides details on how to install the Cofide Connect Control plane’s SPIRE server, which issues SPIFFE identities to the Connect API components, enabling them to communicate with each other and to authenticate clients using mTLS.

Unlike a standard Cofide SPIRE deployment where the Connect API acts as the data source, this server uses a SQL datastore directly. This avoids a circular dependency — the control plane components require identities before they can connect to the Connect API. As a result, identities are configured via Kubernetes custom resources (ClusterSPIFFEIDs) rather than through Connect’s attestation policies.

Example configuration is provided for deployments on GKE and EKS. Other deployment environments are possible - refer to the SPIRE helm chart documentation and the SPIFFE installation guide for full configuration options, or contact us for guidance on your particular use case.

Deploy the SPIRE CRDs and SPIRE chart in the cluster.

The chart follows a three-namespace model, which is the recommended approach when running the Connect control plane in the same cluster as the SPIRE server that grants it identity. spire-mgmt is used as the Helm management namespace, keeping release metadata separate from the runtime components. The SPIRE server runs in spire-server under a restricted Pod Security Standard (PSS) policy, while the SPIRE agent and CSI driver run in spire-system under the privileged PSS policy, as they require host-level access for workload attestation.

Terminal window
helm repo add cofide https://charts.cofide.dev --force-update
helm install spire-crds cofide/spire-crds \
--namespace spire-mgmt \
--create-namespace \
--version 0.5.0-cofide.1 \
--wait \
--timeout 60s
helm install spire cofide/spire \
--values values.yaml \
--namespace spire-mgmt \
--create-namespace \
--version 0.28.3-cofide.3 \
--wait \
--timeout 120s

The sections below document each part of the values.yaml file. Each section can be combined into a single values.yaml to produce a complete configuration.

The global.spire block sets properties shared across all components deployed by the chart.

global:
spire:
namespaces:
create: true
recommendations:
enabled: true
clusterName: <your-cluster-name>
strictMode: true
trustDomain: connect.example.cofide.dev
jwtIssuer: https://oidc-discovery.example.cofide.dev
caSubject:
country: UK
organization: Example
commonName: example.cofide.dev

namespaces.create: true creates all namespaces required by the chart. Individual namespace creation can also be controlled with per-namespace flags.

recommendations.enabled: true applies recommended settings for production deployments.

clusterName labels this cluster within the trust domain.

strictMode: true enforces production-safe configuration defaults and is recommended for all deployments.

trustDomain is the SPIFFE trust domain for the Connect control plane and uniquely identifies all workloads running within it. It is formatted as a domain name but does not need to be resolvable. It should be unique to your organisation to avoid collisions with other deployments, and should not change after workloads start receiving SVIDs.

jwtIssuer is the public URL of the OIDC discovery endpoint and must be reachable by any system that needs to verify JWT-SVIDs issued by this SPIRE server. The hostname must match the one used in the OIDC discovery provider’s service annotations and TLS certificate configured below.

caSubject sets the subject fields embedded in the CA certificate. Replace these with your organisation’s details.

The OIDC discovery provider exposes the SPIFFE OIDC discovery document, enabling external systems to verify JWT-SVIDs.

spiffe-oidc-discovery-provider:
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 3
namespaceOverride: connect

minReplicas: 2 configures a minimum of two replicas for availability.

maxReplicas: 3 allows the HPA to scale up to three replicas under load, while ensuring at least two are always running.

namespaceOverride: connect deploys the OIDC discovery provider into the connect namespace alongside the Connect control plane components.

The service is exposed as an external LoadBalancer. The annotations use GKE’s load balancer controller and external-dns to provision the load balancer and create a DNS record pointing to it.

spiffe-oidc-discovery-provider:
service:
type: LoadBalancer
annotations:
networking.gke.io/load-balancer-type: External
external-dns.alpha.kubernetes.io/hostname: oidc-discovery.example.cofide.dev

tls.spire.enabled controls whether SPIRE manages the TLS certificate for the discovery endpoint. Set it to false when using cert-manager or an existing Kubernetes secret.

cert-manager issues and renews the TLS certificate for the discovery endpoint.

issuer.create: false means the ClusterIssuer is expected to already exist.

spiffe-oidc-discovery-provider:
tls:
spire:
enabled: false
certManager:
enabled: true
issuer:
create: false
certificate:
issuerRef:
kind: ClusterIssuer
name: <your-cluster-issuer>
dnsNames:
- oidc-discovery.example.cofide.dev

The GCP examples use Workload Identity Federation with service account impersonation to grant the SPIRE pod access to GCP services. Add the following annotation to the SPIRE server service account in the combined values.yaml:

spire-server:
serviceAccount:
annotations:
iam.gke.io/gcp-service-account: <spire-server-gcp-service-account>

The AWS examples assume EKS Pod Identity is used to grant the SPIRE pod access to AWS services. If using IRSA instead, add the IAM role annotation to the service account:

spire-server:
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: <your-iam-role-arn>
spire-server:
kind: statefulset
replicaCount: 2
persistence:
storageClass: <your-storage-class>

kind: statefulset is used here to provide each SPIRE server pod with persistent storage.

replicaCount: 2 is the minimum for a highly available deployment, providing redundancy in the event of a pod failure.

The persistence block configures the persistent volume attached to each SPIRE server pod. It is used to store the KMS key identifier file, which allows each pod to locate its signing key in KMS after a restart without regenerating new key material. This is most important when pods may be replaced frequently. If pod replacements are infrequent, the pod name can be used as the key identifier value instead, avoiding the need for persistent storage — though a replaced pod with a new name will generate new key material. Both approaches are described in Key manager below. High-throughput storage is not required. Set storageClass to a storage class available in your cluster.

SPIRE stores registration entries, bundles, and attestation records in a SQL database.

CloudSQL is accessed via the Cloud SQL Auth Proxy running as a sidecar container. The proxy listens on 127.0.0.1:5432 and handles authentication using the GCP service account bound to the pod.

--auto-iam-authn enables IAM database authentication, which means password: unused is intentional (it is included because it is a required field).

sslmode: disable is intentional - the proxy handles TLS between the pod and GCP, so the local loopback connection from SPIRE to the proxy does not need it.

--psc routes the connection through Private Service Connect, keeping traffic on Google’s private network. Remove this flag if you are not using Private Service Connect.

--structured-logs outputs proxy logs in JSON format, making them easier to ingest into a log aggregation pipeline.

The proxy runs as a non-root user with all capabilities dropped.

spire-server:
dataStore:
sql:
databaseType: postgres
databaseName: <your-database-name>
host: 127.0.0.1
port: 5432
username: <spire-server-gcp-service-account-without-gserviceaccount-com>
password: unused
options:
- sslmode: disable
extraContainers:
- name: cloud-sql-proxy
image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.22.0
args:
- --auto-iam-authn
- --port=5432
- --psc
- --structured-logs
- <your-cloudsql-connection-name>
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault

The Cloud SQL proxy version above is the latest at the time of writing. Check the Cloud SQL Auth Proxy releases for the latest version before deploying.

The key manager controls where SPIRE stores the private keys used to sign SVIDs.

GCP KMS stores SPIRE’s signing keys as versioned asymmetric keys.

key_ring is the full GCP resource path to the KMS key ring.

disk.enabled: false disables the default disk-based key manager so that GCP KMS is used exclusively.

SPIRE needs a stable identifier per server instance to look up the correct KMS key after a pod restart. Two approaches are supported.

Key identifier file stores the identifier on the persistent volume attached to each StatefulSet pod:

spire-server:
keyManager:
disk:
enabled: false
unsupportedBuiltInPlugins:
keyManager:
gcp_kms:
plugin_data:
key_ring: <your-gcp-key-ring-identifier>
key_identifier_file: /run/spire/data/key_id

Key identifier value uses a string as the identifier, avoiding the need to store the key ID on the persistent volume. Use the Kubernetes Downward API to inject the pod name as an environment variable so that each replica uses a distinct, stable identifier. StatefulSet pod names are stable across restarts, so each pod consistently maps to the same KMS key. If pods are frequently cycled with new names (for example, in a Deployment), each new name causes a new KMS key to be created; SPIRE has mechanisms to identify and remove keys that are no longer in use, but this may result in higher key churn. The value must contain only lowercase letters, numbers, underscores, and dashes, and must not exceed 63 characters — StatefulSet pod names satisfy this constraint.

spire-server:
extraEnv:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
keyManager:
disk:
enabled: false
unsupportedBuiltInPlugins:
keyManager:
gcp_kms:
plugin_data:
key_ring: <your-gcp-key-ring-identifier>
key_identifier_value: $POD_NAME

By default, SPIRE acts as its own root CA. An upstream authority can optionally be configured to chain SPIRE’s CA to an external root, in which case SPIRE obtains a signed intermediate CA certificate from the upstream and uses that to issue SVIDs to workloads. SVIDs issued by this SPIRE server will chain up to the upstream root CA, so any relying party that already trusts the upstream CA will also trust these SVIDs without additional trust configuration.

GCP Certificate Authority Service acts as the root CA.

root_cert_spec identifies which CA pool to use and which CA within that pool via a label selector (label_key and label_value).

spire-server:
unsupportedBuiltInPlugins:
upstreamAuthority:
gcp_cas:
plugin_data:
root_cert_spec:
project_name: <your-gcp-project-name>
region_name: <your-gcp-region>
ca_pool: <your-ca-pool>
label_key: <your-ca-label-key>
label_value: <your-ca-label-value>

The node attestor is how SPIRE verifies the identity of the Kubernetes node that a workload is running on before issuing an SVID.

spire-server:
nodeAttestor:
k8sPSAT:
audience:
- spire-server

k8sPSAT (Kubernetes Projected Service Account Tokens) is the recommended attestor for Kubernetes deployments.

audience ensures the projected service account tokens are restricted to the SPIRE server, preventing them from being used to authenticate with other services in the cluster.

ClusterSPIFFEIDs define which pods receive SPIFFE identities and what form those identities take. They are managed by the SPIRE controller manager. This configuration issues SVIDs to the Connect control plane components so that they can communicate with each other and to authenticate clients using mTLS. See the introduction above for why ClusterSPIFFEIDs are used here rather than Connect’s attestation policies.

spire-server:
controllerManager:
identities:
clusterSPIFFEIDs:
default:
enabled: false
namespace-connect:
spiffeIDTemplate: 'spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}'
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: connect

default.enabled: false disables the catch-all ClusterSPIFFEID that would otherwise issue identities to every pod in the cluster. Explicit identities are defined instead.

namespace-connect issues SPIFFE IDs to all pods in the connect namespace, i.e. the Connect control plane components. spiffeIDTemplate uses Go template syntax to produce SPIFFE IDs in the form spiffe://{trust-domain}/ns/{namespace}/sa/{service-account}, giving each component a distinct identity based on its Kubernetes service account.

spire-server:
telemetry:
prometheus:
enabled: true

This enables Prometheus metrics scraping on the SPIRE server pod and is recommended for production deployments.