Simple tutorial to secure an API you deploy on Cloud Run behind GCP API Gateway
Basically
client --> API Gateway --> API Server
where each of the components is secure and will only allow authenticated clients through.
We will deploy each of these components from scratch and add on the IAM permissions required as well as the JWT Auth audience checks
export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format='value(projectNumber)'`
Create Service accounts:
# Service account the API Gateway runs as
gcloud iam service-accounts create gateway-sa --display-name "Service Account for API Gateway"
# Service account your API Server runs as
gcloud iam service-accounts create api-sa --display-name "Service Account for API"
# Service account and key a client will use to call the gateway
gcloud iam service-accounts create api-client-sa --display-name "Service Account for API Client"
gcloud iam service-accounts keys create svc_client.json --iam-account=api-client-sa@$PROJECT_ID.iam.gserviceaccount.com
# build and deploy
cd app/
gcloud builds submit --tag gcr.io/$PROJECT_ID/apiserver .
gcloud beta run deploy apiserver --image gcr.io/$PROJECT_ID/apiserver \
--region us-central1 --platform=managed -q
We need to find the auto-assigned URL for this api server...we're only doing this because i'm paranoid and have a handler in the deployed API that always decodes and verifies the inbound header (even if Cloud Run would validate it!)
eg. see authMiddleware()
middleware function in main.go
.
router := mux.NewRouter()
router.Methods(http.MethodGet).Path("/todo").HandlerFunc(listhandler)
var server *http.Server
server = &http.Server{
Addr: *httpport,
Handler: authMiddleware(router),
}
It is critical to use the authMiddleware
check gainst the inbound Authorization
if this app runs standalone without an immediate intercepting proxy (eg, if the api gateway someday allows traffic to prem apps). You can also chain the middleware handlers to decode and validate the X-Forwarded-Authorization
JWT that the gateway forwards. This JWT header is sent by the client to gateway and is finally forwarded to the app. If the inbound JWT token to the API gateway is a self-signed JWT (see Using JWT to authenticate Users), it could contain custom claims to validate (eg authorization claims).
Anyway, fist find the URL
export ALLOWED_AUDIENCE_URL=`gcloud run services describe apiserver --format="value(status.url)"`/todo
echo $ALLOWED_AUDIENCE_URL
Then specify that URL as the audience to check for as an argument to the API like this:
gcloud beta run deploy apiserver --image gcr.io/$PROJECT_ID/apiserver \
--region us-central1 --platform=managed \
--service-account=api-sa@$PROJECT_ID.iam.gserviceaccount.com \
--args="--allowedAudience=$ALLOWED_AUDIENCE_URL" -q
At this point, the API backend always doublechecks the audience and will
Now allow the Gateway's Service account permissions to access the API backend
gcloud run services add-iam-policy-binding apiserver --region us-central1 \
--platform=managed --member=serviceAccount:gateway-sa@$PROJECT_ID.iam.gserviceaccount.com \
--role=roles/run.invoker
Edit openapi2-run.yaml
, enter in the values here that you have for the backend API (remember to keep the trailing /todo
)
x-google-backend:
address: $ALLOWED_AUDIENCE_URL/todo
now Create the gateway Config
gcloud beta api-gateway api-configs create config-1 --api=api-1 --openapi-spec=openapi-run.yaml \
--backend-auth-service-account=gateway-sa@$PROJECT_ID.iam.gserviceaccount.com
Create the Gateway
gcloud beta api-gateway gateways create gateway-1 \
--api=api-1 --api-config=config-1 \
--location=us-central1
We need to "secure" the gateway with security specification which allows ANY google signed OIDC token with the right audience through. Right, we're securing it such that any google issued OIDC token is allowed through that has the audience value....
Find the hostname of the gateway
export GATEWAY_HOSTNAME=`gcloud beta api-gateway gateways describe gateway-1 --location=us-central1 --format="value(defaultHostname)"`
Then openapi2-run.yaml
and enter value under the security definitions of x-google-audiences
securityDefinitions:
google_id_token:
authorizationUrl: ""
flow: "implicit"
type: "oauth2"
x-google-issuer: "https://accounts.google.com"
x-google-jwks_uri: "https://www.googleapis.com/oauth2/v3/certs"
x-google-audiences: "https://$GATEWAY_HOSTNAME"
As mentioned, this allows any OIDC token that happens to have the right audience value through. If you wanted to make this a private API, you would use a different JWT issuer (eg, a service account you own)
Now deploy as config-2
gcloud beta api-gateway api-configs create config-2 --api=api-1 --openapi-spec=openapi-run.yaml \
--backend-auth-service-account=gateway-sa@$PROJECT_ID.iam.gserviceaccount.com
gcloud beta api-gateway gateways update gateway-1 \
--api=api-1 --api-config=config-2 \
--location=us-central1
We're ready now to call the gatway
First bootstrap gcloud with the client's service account
gcloud auth activate-service-account --key-file=`pwd`/svc_client.json
Then get an OIDC token that has an audience that matches the gateway
export ID_TOKEN=`gcloud auth print-identity-token --audiences=https://$GATEWAY_HOSTNAME`
Note the OIDC token's issuer and audience:
{
"aud": "https://gateway-1-do52xz04.uc.gateway.dev",
"azp": "[email protected]",
"email": "[email protected]",
"email_verified": true,
"exp": 1602882463,
"iat": 1602878863,
"iss": "https://accounts.google.com",
"sub": "108281515051350346015"
}
now call the gateway with the token
curl -v -H "Authorization: Bearer $ID_TOKEN" https://$GATEWAY_HOSTNAME/todo
What you should see is the "api" backend on cloud run returning the serivce account it saw in the Authorization
header it got. In our case its a token that identifies the inbound requestor (i.,e the gateway)
That is...its a simple api doing simple things....and yeah, i "secured" the gateway to allow any google OIDC token with the right audience...in reality, you'll likely want to use yoru own service account or other JWT issuer.
You can also integrate the gateway and cloud run to support users and enriched webapps....a colleague of mine is gonna write that up in a bit
So...here are the headers and decoded JWTs
This is the JWT token issued by the client. Note the audience and issuer
gcloud auth print-identity-token --audiences=https://gateway-1-do52xz04.uc.gateway.dev
{
"aud": "https://gateway-1-do52xz04.uc.gateway.dev",
"azp": "[email protected]",
"email": "[email protected]",
"email_verified": true,
"exp": 1602882463,
"iat": 1602878863,
"iss": "https://accounts.google.com",
"sub": "108281515051350346015"
}
I used gcloud to create this OIDC token but you can ofcourse use any other language/api see Authenticating using Google OpenID Connect Tokens
These are the header and JWTs as seen byt your API server
X-Forwarded-Authorization
The original header sent by the client
{
"aud": "https://gateway-1-do52xz04.uc.gateway.dev",
"azp": "[email protected]",
"email": "[email protected]",
"email_verified": true,
"exp": 1602882463,
"iat": 1602878863,
"iss": "https://accounts.google.com",
"sub": "108281515051350346015"
}
X-Apigateway-Api-Userinfo
This is the same as above except that its not a signed JWT (its just the claims part)
{
"aud": "https://gateway-1-do52xz04.uc.gateway.dev",
"azp": "[email protected]",
"email": "[email protected]",
"email_verified": true,
"exp": 1602882463,
"iat": 1602878863,
"iss": "https://accounts.google.com",
"sub": "108281515051350346015"
}
Authorization
This is the header that is sent by the gateway to your api server that gets validated by IAM
{
"aud": "https://apiserver-6w42z6vi3q-uc.a.run.app/todo",
"azp": "108234776417428586904",
"email": "[email protected]",
"email_verified": true,
"exp": 1602881043,
"iat": 1602877443,
"iss": "https://accounts.google.com",
"sub": "108234776417428586904"
}