Workload Identity Federation with SPIRE

Introduction

What are SPIFFE and SPIRE?

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.

What is Workload Identity Federation?

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.

In this demo

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!

Set up Spire

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.

Set up CA certs

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.pem

Start the server

rm -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.conf

The 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.

Start the agent

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 healthcheck

Create registration policy

We 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:501

In 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-service

Later on, we'll use information from the Subject Alternate Name (SAN) as our identity.

Set up Workload Identity Federation

Registering our Certificate Authority

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:

Google IAM setup

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.

Authenticating to GCP

Option 1 - using curl

We 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": [
  #   ...

Option 2 - using gcloud

Let'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-service

There 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.

Thanks

This blog post was guided by this blog post.

The setup of the Spire server was mostly copied from the official tutorial.


  1. SVID: SPIFFE Verifiable Identity Document, typically in the form of an ID plus an x509 certificate signed by the agent and server.↩︎

Back to all posts