SPIFFE is a set of principles that forms a framework for managing identities and authorisation of workloads. It forms the basis of a zero trust architecture, which aims to provide secure workload communication in a scenario where the network is considered impossible to secure. Concretely, this means we want to encrypt and authenticate all traffic.
SPIRE is an implementation of SPIFFE. There are other (partial) implementations of the SPIFFE specification, for example Istio.
I'll write both names as lowercase, because I find it easier to read.
Spire runs in a hierarchical setup. In a simple cluster, you could have one Spire server, and one agent per node in the cluster.
Servers verify the identity of agents. Agents can, upon request, provide a proof of identity (an SVID1) to workloads running on the same node.
Spiffe identities look as follows:
spiffe://my-trust-domain.com/my-service. You see the
trust domain or root, that the server is authoritative for, and the
workload-specific portion.
For an overview on how trust is bootstrapped, see the Spire documentation.
When workloads need to access Google Cloud Platform (GCP) resources, they need to be authenticated and authorised. The identities we use are called service accounts.
Workloads on GCP can authenticate as a service account by
querying a metadata server for proof of identity. The metadata
server can only be access from localhost, which is how
it is uniquely tied to one virtual machine.
Workloads that do not have access to a metadata server can use JSON files that contain the service account credentials. For example; you could download these credentials to your laptop and use them to run a workload locally. You can also put these credentials on a machine in Amazon EC2, and authenticate your calls from the EC2 VM to a Google API with it.
This is a hassle and a security risk, because it is easy to leak or lose the JSON credentials.
To skip the JSON files, GCP allows you register trusted identity providers and define IAM rules using the external (non-GCP) identities.
We'll run Spire locally (on Linux or OSX), and for a workload to get an SVID.
We then set up Workload Identity Federation, so the Spire identities can be used inside GCP.
And then we demonstrate how IAM rules are defined in GCP that authorise a workload, using its Spire identity.
This should allow us to use gcloud using our Spire
identity, managed locally on my laptop.
All of this without creating any GCP service accounts or JSON credential files!
I'm writing this on my Mac. To stay close to Linux' behavior, I
recommmend you brew install coreutils. Then you can use
utilities like sed with the same flags as on Linux.
They may be renamed with a g suffix, e.g.
gsed.
We begin by creating root certificates for our Spire cluster. These certificates sign all other identities.
In a production setup, these should be stored more securely (e.g. in a hardware security module).
openssl genrsa -out certs/myCA.key 2048
openssl req -x509 -new -nodes -sha256 -days 1825 \
-key certs/myCA.key \
-out certs/myCA.pemrm -Rf .data # Cleaning up any old Spire server data
cat server.conf
# server {
# bind_address = "127.0.0.1"
# bind_port = "8081"
# socket_path = "/tmp/spire-server/private/api.sock"
# trust_domain = "spire.cxcc.nl"
# data_dir = "./.data"
# log_level = "DEBUG"
# }
# plugins {
# DataStore "sql" {
# plugin_data {
# database_type = "sqlite3"
# connection_string = "./.data/datastore.sqlite3"
# }
# }
# NodeAttestor "join_token" {
# plugin_data {
# }
# }
# KeyManager "memory" {
# plugin_data = {}
# }
# UpstreamAuthority "disk" {
# plugin_data {
# key_file_path = "certs/myCA.key"
# cert_file_path = "certs/myCA.pem"
# }
# }
# }
bin/spire-server run -config server.confThe server will keep running - don't close this terminal.
With the server running, we can create a token to allow an agent to register.
In production, this can be automated via several plugins to chose a method for node attestation. In this demo, we just pass around a secret token to "prove" the identity of whoever knows it.
# Verify the server is healthy
bin/spire-server healthcheck
# Token to authorise new agent joining
export TOKEN="$(bin/spire-server token generate \
-spiffeID spiffe://spire.cxcc.nl/agent-1 \
| awk '{print $2}')"Use this token in the next section.
We now start the Spire agent. Workloads on the node (my laptop) will interact with the agent to request proof of their identities.
bin/spire-agent run -config agent.conf -joinToken $TOKEN;This agent also keeps running - don't close this terminal either.
In a new terminal window, run:
# Verify the agent is healthy
bin/spire-agent healthcheckWe must tell the Spire server that we want to allow the agent to sign identities, and specify the format that we want to follow.
The server will distribute that config to the agents, so we don't need to explicitly configure the agents.
bin/spire-server entry create \
-parentID spiffe://spire.cxcc.nl/agent-1 \
-spiffeID spiffe://spire.cxcc.nl/my-service \
-selector unix:uid:$(id -u)
# Entry ID : 6af1c5c7-75e4-4e4c-aa56-f0d8fdb54227
# SPIFFE ID : spiffe://spire.cxcc.nl/my-service
# Parent ID : spiffe://spire.cxcc.nl/agent-1
# Revision : 0
# X509-SVID TTL : default
# JWT-SVID TTL : default
# Selector : unix:uid:501In this case, we use the
unix workload attestor. This attestor allows the
agent to give processes on the node an identity based on their Unix
process ID.
We can now fetch a key using the registration policy. This will
use the uid of the current user (which you can verify
by running id or id -u $(whoami) in your
terminal).
bin/spire-agent api fetch x509 -write ./tmp
# Received 1 svid after 253.24875ms
#
# SPIFFE ID: spiffe://spire.cxcc.nl/my-service
# ...
# Writing SVID #0 to file tmp/svid.0.pem.
# Writing key #0 to file tmp/svid.0.key.
# Writing bundle #0 to file tmp/bundle.0.pem.I mentioned earlier that SVIDs are typically x509 certificates. Let's inspect our SVID using openssl (I omitted some fields):
openssl x509 -in tmp/svid.0.pem -text -noout
# Certificate:
# Data:
# Version: 3 (0x2)
# Signature Algorithm: ecdsa-with-SHA256
# Issuer: C=US, O=SPIFFE, serialNumber=67421474041745768253204596110821913017
# Validity
# Not Before: Feb 16 22:10:39 2025 GMT
# Not After : Feb 16 23:10:49 2025 GMT
# Subject: C=US, O=SPIRE
# X509v3 extensions:
# X509v3 Key Usage: critical
# Digital Signature, Key Encipherment, Key Agreement
# X509v3 Extended Key Usage:
# TLS Web Server Authentication, TLS Web Client Authentication
# X509v3 Basic Constraints: critical
# CA:FALSE
# X509v3 Subject Alternative Name:
# URI:spiffe://spire.cxcc.nl/my-serviceLater on, we'll use information from the Subject Alternate Name (SAN) as our identity.
Usually x509 certificates are verified against a known certificate authority (CA) that everyone agreed to trust.
Since we generated our own certificates, they are not trusted by anyone else. We have to tell Google to trust our certificates.
Keep in mind, in the first section we generated the following files:
certs/myCA.pem our CA certificate, which is not
secret. We must share it with anyone that should be able to verify
signatures. When someone gives you a certificate like this, you
should only trust the certificate if you trust whoever gave it, and
that it wasn't changed (e.g. never trust a certificate if you
retrieved it over an insecure protocol, such as http).certs/myCA.key our CA private key, which is secret.
Only the SPIRE server should have access to this. The commands below
never use this (we only use the certificate).tmp/svid.0.pem our workload certificate.tmp/svid.0.key our workload private key. It is
created by SPIRE, and signed with our CA key. We will use this to
authenticate to GCP.We'll set a few variables, so the commands follow are easier to read.
export PROJECT_NUMBER="133713371337" # Your GCP project number
export PROJECT_ID="my-project" # Your GCP project ID
export POOL_ID="cxcc-wif-pool"
# The value you see in:
# openssl x509 -in ./tmp/svid.0.pem -text -noout | grep -A1 'X509v3 Subject Alternative Name'
export SUBJECT_ATTRIBUTE_VALUE="spire.cxcc.nl/my-service"
export PROVIDER_ID="my-provider"We now create an identity pool, and a provider to use that pool
gcloud iam workload-identity-pools create $POOL_ID \
--location="global" \
--description="CXCC demo pool" \
--display-name="my-pool"
# Create a YAML file that has our CA certificate (encoded without newlines)
export ROOT_CERT=$(cat certs/myCA.pem | sed 's/^[ ]*//g' | gsed -z '$ s/\n$//' | tr '\n' $ | sed 's/\$/\\n/g')
cat << EOF > trust_store.yaml
trustStore:
trustAnchors:
- pemCertificate: "${ROOT_CERT}"
EOF
# Note - as of 2025-01-15, this requires alpha feature access
# https://cloud.google.com/iam/docs/workload-identity-federation-with-x509-certificates
gcloud iam workload-identity-pools providers create-x509 $PROVIDER_ID \
--location=global \
--workload-identity-pool="$POOL_ID" \
--trust-store-config-path="./trust_store.yaml" \
--attribute-mapping="google.subject=assertion.san.uri.split('/')[3]" \
--billing-project="my-project" The attribute mapping controls the name of identities. In this
case, the CEL logic would turn
spiffe://spire.cxcc.nl/my-service into
my-service. You can try CEL rules here.
This is all the setup that is required!
Google now knows what CA to trust, and the CA (your SPIRE server) signed the SVID (an x509 certificate) of the workload.
curlWe can use a fairly low-level way to authenticate - using only
curl!
export AUDIENCE="//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID"
curl --key ./tmp/svid.0.key \
--cert ./tmp/svid.0.pem \
--request POST 'https://sts.mtls.googleapis.com/v1/token' \
--header "Content-Type: application/json" \
--data-raw '{
"subject_token_type": "urn:ietf:params:oauth:token-type:mtls",
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"audience": "'$AUDIENCE'",
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"scope": "https://www.googleapis.com/auth/cloud-platform",
}'
# {
# "access_token": "1234-my-token-5678",
# "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
# "token_type": "Bearer",
# "expires_in": 3599
# }This clearly shows that we are using mTLS.
The access token you get here you can include in follow-up API calls, e.g.:
# Run this with your personal account, to set up IAM
gcloud projects add-iam-policy-binding my-project \
--member='principal://iam.googleapis.com/projects/133713371337/locations/global/workloadIdentityPools/demo-wif-pool/subject/my-service' \
--role='roles/storage.admin' \
--condition=None
# Now use the identity we granted permission to.
# Take the token you got back from the curl above.
curl -X GET -H "Authorization: Bearer 1234-my-token-5678" \
"https://storage.googleapis.com/storage/v1/b?project=my-project"
# {
# "kind": "storage#buckets",
# "items": [
# ...gcloudLet's see how this works when we use gcloud instead,
so we see the more "developer friendly" interface.
export CLIENT_CERT_PATH="$(pwd)/tmp/svid.0.pem"
export CLIENT_KEY_PATH="$(pwd)/tmp/svid.0.key"
gcloud iam workload-identity-pools create-cred-config \
"projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID" \
--credential-cert-path $CLIENT_CERT_PATH \
--credential-cert-private-key-path $CLIENT_KEY_PATH \
--output-file=wi-config.json
# Created credential configuration file [wi-config.json].
# Created enterprise-certificate-proxy configuration file [/Users/cxcc/.config/gcloud/certificate_config.json].
gcloud auth login --cred-file=./wi-config.json
gcloud auth list
# ACTIVE ACCOUNT
# * principal://iam.googleapis.com/projects/133713371337/locations/global/workloadIdentityPools/cxcc-wif-pool/subject/my-serviceThere you have it!
We can now interact with GCP, using identities that we create outside GCP.
And we can assign IAM permissions to these identities, without needing to create intermediate serviceaccounts.
This blog post was guided by this blog post.
The setup of the Spire server was mostly copied from the official tutorial.
SVID: SPIFFE Verifiable Identity Document, typically in the form of an ID plus an x509 certificate signed by the agent and server.↩︎