GCP Workload Federation with x509 certificates where the private key is either
- embedded in a
Trusted Platform Module (TPM) - provided by an anything that implements a golang crypto.Signer (eg, HSM, TPM, PKCS11, Yubikey, ordinary PEM private keys)
Currently, GCP Workload Federation requires the client key to be accessible directly as a file.
The samples in this repo demonstrates how to bootstrap credentials where the mTLS private key is embedded inside a device.
Also note that you can BIND the issued access_token such that the token's use must be used with the same TLS client certificates.
WARNING: this repo is not supported by Google
Also see:
-
Federation -
mtls -
crypto.Signer -
JWT Signers -
TPM
The following end-to-end demo uses a software tpm (swtpm) for simplicity.
The rough steps are:
- create a local CA
- create a server certificate for testing
- start a software TPM locally
- generate an private key on the TPM
- use openssl to issue a CSR
- generate a client certificate
- test client and server certificate locally
- configure workload federation using local ca
- add IAM binding for federated identity
- access gcp resource after getting token using client certificate
To use this sample end-to-end, you'll need
openssl3- tpm2-openssl
- tpm2-tools
go- Software TPM (optional)
### Create CA
cd example/
git clone https://github.com/salrashid123/ca_scratchpad.git
cd ca_scratchpad/
mkdir -p ca/root-ca/private ca/root-ca/db crl certs
chmod 700 ca/root-ca/private
cp /dev/null ca/root-ca/db/root-ca.db
cp /dev/null ca/root-ca/db/root-ca.db.attr
echo 01 > ca/root-ca/db/root-ca.crt.srl
echo 01 > ca/root-ca/db/root-ca.crl.srl
openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 \
-pkeyopt ec_param_enc:named_curve \
-out ca/root-ca/private/root-ca.key
export SAN=single-root-ca
openssl req -new -config single-root-ca.conf -key ca/root-ca/private/root-ca.key \
-out ca/root-ca.csr
openssl ca -selfsign -config single-root-ca.conf \
-in ca/root-ca.csr -out ca/root-ca.crt \
-extensions root_ca_extexport PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format='value(projectNumber)'`
export POOL_ID="cert-pool-1"
export PROVIDER_ID="cert-provider-1"
## you may need to allow "MTLS" as an org policy
# export ORGANIZATION_ID=123456789
# $ gcloud resource-manager org-policies describe constraints/iam.workloadIdentityPoolProviders --organization=$ORGANIZATION_ID
# constraint: constraints/iam.workloadIdentityPoolProviders
# listPolicy:
# allowedValues:
# - MTLS
### >>> NOTE: ca/root-ca.crt must be just the PEM file, no headers
# openssl x509 -in ca/root-ca.crt -text \
# -certopt no_header,no_pubkey,no_subject,no_issuer,no_signame,no_version,no_serial,no_validity,no_extensions,no_sigdump,no_aux,no_extensions
export ROOT_CERT=$(cat ca/root-ca.crt | sed 's/^[ ]*//g' | sed -z '$ s/\n$//' | tr '\n' $ | sed 's/\$/\\n/g')
cat << EOF > trust_store.yaml
trustStore:
trustAnchors:
- pemCertificate: "${ROOT_CERT}"
EOF
gcloud iam workload-identity-pools create $POOL_ID \
--location="global" \
--description="Certificate Pool" \
--display-name="Certificate Pool 1"
## note, if you have the org policy constraints/iam.workloadIdentityPoolProviders set, you will need to allow "MTLS"
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.subject.dn.cn" \
--billing-project="$PROJECT_ID"
## add an iam policy binding.
# Since w'ere using the default attribute mapping, the CN= value in the cert is the subject.
# In our case, its going to be "workload1" in a later step
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member "principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/subject/workload1" \
--role=roles/storage.adminCreate a test server certificate. This cert is used later on to test locally before invoking workload federation:
### create server certificate
export NAME=server
export SAN="DNS:server.domain.com"
openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 \
-pkeyopt ec_param_enc:named_curve \
-out certs/$NAME.key
openssl req -new -config server.conf \
-out certs/$NAME.csr \
-key certs/$NAME.key \
-subj "/C=US/O=Google/OU=Enterprise/CN=server.domain.com"
openssl ca \
-config single-root-ca.conf \
-in certs/$NAME.csr \
-out certs/$NAME.crt \
-extensions server_extStart software TPM
rm -rf /tmp/myvtpm && mkdir /tmp/myvtpm && \
sudo swtpm_setup --tpmstate /tmp/myvtpm --tpm2 --create-ek-cert && \
sudo swtpm socket --tpmstate dir=/tmp/myvtpm --tpm2 --server type=tcp,port=2321 --ctrl type=tcp,port=2322 --flags not-need-init,startup-clear --log level=2
## in a new window
export TPM2TOOLS_TCTI="swtpm:port=2321"
export TPM2OPENSSL_TCTI="swtpm:port=2321"
export TPM2TSSENGINE_TCTI="swtpm:port=2321"
export OPENSSL_MODULES=/usr/lib/x86_64-linux-gnu/ossl-modules/
# or wherever tpm2.so sits, eg /usr/lib/x86_64-linux-gnu/ossl-modules/tpm2.so
# export TSS2_LOG=esys+debug
$ openssl list --providers -provider tpm2
Providers:
tpm2
name: TPM 2.0 Provider
version: 1.2.0-25-g87082a3
status: activecreate client key and certificate
## create h2 primary
printf '\x00\x00' > certs/unique.dat
tpm2_createprimary -C o -G ecc -g sha256 \
-c certs/rprimary.ctx \
-a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|noda|restricted|decrypt" -u certs/unique.dat
### create key
export NAME=workload1
export SAN="URI:spiffie://domain/workload1"
tpm2_create -G ecc:ecdsa -g sha256 -u certs/rkey.pub -r certs/rkey.priv -C certs/rprimary.ctx
tpm2_flushcontext -t
tpm2_load -C certs/rprimary.ctx -u certs/rkey.pub -r certs/rkey.priv -c certs/rkey.ctx
tpm2_print -t TPM2B_PUBLIC certs/rkey.pub
### make it persistent
tpm2_evictcontrol -C o -c certs/rkey.ctx 0x81008001
tpm2_flushcontext -t
## convert the key public/private --> PEM
tpm2_encodeobject -C primary.ctx -u certs/rkey.pub -r certs/rkey.priv -o certs/$NAME.pem
openssl ec -provider tpm2 -provider default -in certs/$NAME.pem --text
# todo, use openssl to gen the key, unfortunately, this key doesn't encode all the scheme values
## openssl genpkey -provider tpm2 -provider default -algorithm ec -pkeyopt ec_paramgen_curve:P-256 -out certs/rkey2.pem
## /usr/bin/tpm2tss-genkey --public=certs/rkey.pub --private=certs/rkey.priv --out=certs/rkey_test.pem
## tpm2_print -t TPM2B_PUBLIC certs/rkey2.pub
openssl req -provider tpm2 -provider default -new \
-config client.conf \
-out certs/$NAME.csr \
-key certs/$NAME.pem \
-subj "/C=US/O=Google/OU=Enterprise/CN=workload1"
openssl ca \
-config single-root-ca.conf \
-in certs/$NAME.csr \
-out certs/$NAME.crt \
-extensions client_extNote, certs/$NAME.pem is the PEM formatted TPM file (its still encrypted by the TPM):
eg
$ cat certs/$NAME.pem
-----BEGIN TSS2 PRIVATE KEY-----
MIHyBgZngQUKAQOgAwEB/wIEQAAAAQRaAFgAIwALAAQAcgAAABAAGAALAAMAEAAg
WbN4jKEVyh74//nT30nkTsgSG6rmVhiBYtOSSquE42kAIFcJhcqTvOohhzYWtZvI
XxJzRGyvNSEz2c601jFQBKGGBIGAAH4AIJQuNactvVACOQb6aUM1f82z1nvI77bJ
zcy06z7FOhZlABBgyf5keg6kbMYVmoWL5Jz844e8RJYmj0Kuv2trWbmueKbX0khO
itmepjBiswClYzq3cZKZTTpEeHAlmK7TK7BCrK5oaXP34VjBzJ4RIysrQUBWARR3
hBqKKb0=
-----END TSS2 PRIVATE KEY-----To test the TPM locally, run a simple local webserver server
## as openssl
openssl s_server -cert certs/server.crt -key certs/server.key \
-accept 8443 -CAfile ca/root-ca.crt -Verify 5 -tls1_3 -WWW -tlsextdebug
## or as golang
go run server/main.goTo test the client key on the TPM
openssl s_client -provider tpm2 -provider default \
-connect localhost:8443 \
-servername server.domain.com \
-CAfile ca/root-ca.crt \
-cert certs/$NAME.crt \
-key handle:81008001 \
-tls1_3 -state \
-tlsextdebug \
--verify 5 -showcerts
#### enter GET / HTTP/1.1<enter><enter>
## or as go with TPM
go run client_tpm/client.go
### as go with a crypto.Signer
go run client_singer/client.goBoth should return a simple "ok" back from the the servers
Now we're ready to use the TPM based keys against a GCP resource,
The following will initialize the TPM key, use it for mtls with sts server, get an access_token, then access gcs
## Using TPM
go run gcp_tpm/client.go \
--projectId=$PROJECT_ID --projectNumber=$PROJECT_NUMBER \
--poolid="$POOL_ID" --providerid="$PROVIDER_ID" \
-pubCert=ca_scratchpad/certs/workload1.crt \
--keyfile=ca_scratchpad/certs/workload1.pem
## Using Signer
go run gcp_signer/client.go \
--projectId=$PROJECT_ID --projectNumber=$PROJECT_NUMBER \
--poolid="$POOL_ID" --providerid="$PROVIDER_ID" \
-pubCert=ca_scratchpad/certs/workload1.crt \
--keyfile=ca_scratchpad/certs/workload1.pem
The TPM is actually invoked to sign (TPM2_CC_SIGN):
If you had audit logging enabled, you'd see the sts exchange
as a full log, it would be
{
"protoPayload": {
"@type": "type.googleapis.com/google.cloud.audit.AuditLog",
"status": {},
"authenticationInfo": {},
"requestMetadata": {
"callerIp": "22.127.34.114",
"callerSuppliedUserAgent": "Go-http-client/2.0",
"requestAttributes": {
"time": "2024-10-21T23:25:19.662597395Z",
"auth": {}
},
"destinationAttributes": {}
},
"serviceName": "sts.googleapis.com",
"methodName": "google.identity.sts.v1.SecurityTokenService.ExchangeToken",
"authorizationInfo": [
{
"permission": "sts.identityProviders.checkLogging",
"permissionType": "ADMIN_READ"
}
],
"resourceName": "projects/995081011211/locations/global/workloadIdentityPools/cert-pool-1/providers/cert-provider-1",
"request": {
"@type": "type.googleapis.com/google.identity.sts.v1.ExchangeTokenRequest",
"audience": "//iam.googleapis.com/projects/995081011211/locations/global/workloadIdentityPools/cert-pool-1/providers/cert-provider-1",
"grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
"requestedTokenType": "urn:ietf:params:oauth:token-type:access_token",
"subjectTokenType": "urn:ietf:params:oauth:token-type:mtls"
},
"metadata": {
"mapped_principal": "principal://iam.googleapis.com/projects/995081011211/locations/global/workloadIdentityPools/cert-pool-1/subject/workload1",
"keyInfo": [
{
"certificateType": "trust_anchor",
"fingerprintSha256": "nJSP1ltx8pkoC8effgA8/OjvOAN2IsQtTIUHohdw3WQ",
"timeUntilExpiration": "315413092s",
"use": "verify"
}
],
"@type": "type.googleapis.com/google.identity.sts.v1.AuditData"
}
},
"insertId": "1ipslzbdmoxk",
"resource": {
"type": "audited_resource",
"labels": {
"service": "sts.googleapis.com",
"project_id": "core-eso",
"method": "google.identity.sts.v1.SecurityTokenService.ExchangeToken"
}
},
"timestamp": "2024-10-21T23:25:19.653928185Z",
"severity": "INFO",
"logName": "projects/core-eso/logs/cloudaudit.googleapis.com%2Fdata_access",
"receiveTimestamp": "2024-10-21T23:25:20.080540221Z"
}Note, the fingerprintSha256 is for the trust_anchor; not the client certificate
$ openssl x509 -in ../ca/root-ca.crt -outform DER | openssl sha256
SHA2-256(stdin)= 9c948fd65b71f299280bc79f7e003cfce8ef38037622c42d4c8507a21770dd64
hex->base64("9c948fd65b71f299280bc79f7e003cfce8ef38037622c42d4c8507a21770dd64") =>
"nJSP1ltx8pkoC8effgA8/OjvOAN2IsQtTIUHohdw3WQ"If you had storage auditlogs enabled, you'd see:
Since this is a demo, we'll also show the transport trace logs
- for local:
sudo tcpdump -s0 -ilo -w client_local_trace port 8443
export SSLKEYLOGFILE=client_local_keylog.log
go run client_tpm/client.go
# then
wireshark client_local_trace.cap -otls.keylog_file:client_local_keylog.log- for GCP
sudo tcpdump -s0 -iany -w client_gcp_trace.cap host sts.mtls.googleapis.com
export SSLKEYLOGFILE=client_gcp_keylog.log
go run gcp_tpm/client.go
# then
wireshark client_gcp_trace.cap -otls.keylog_file:client_gcp_keylog.log



